diff --git a/README.md b/README.md index b9abc86f..950490eb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # StreamFlow Timelock -Token Vesting and Streaming Payments for SPL tokens. Free and open-source. +Token Vesting and Streaming Payments for SPL tokens. **Free and open-source.** Backed by Serum and Solana. @@ -8,10 +8,19 @@ Backed by Serum and Solana. This software is under active development. It's provided as is, without any warranty. -**The code is not audited.** +**The code is not yet audited.** ### System overview +System has 4 composable layers. There are (top to bottom): + +- `streamflow-app` — React/TypeScript [web application that hosts user interface](https://app.streamflow.finance). +- `@streamflow/timelock` — a [NPM package](https://www.npmjs.com/package/@streamflow/timelock) used by the web app. + Interacts with provided `timelock` program deployed on Solana chain. +- `timelock` — simple implementation of Solana/Anchor program that integrates `timelock-crate` (described below). +- `timelock-crate` — a crate that provides `create`, `withdraw`, `cancel`, `transfer` stream/vesting contract + functionalities out of the box. Can be used in other Solana/Anchor programs, as demonstrated here. + ![Platform overview](/misc/platform.png) ### Legal diff --git a/packages/timelock/idl.ts b/packages/timelock/idl.ts index d8f9f2fc..e9a3df79 100644 --- a/packages/timelock/idl.ts +++ b/packages/timelock/idl.ts @@ -96,10 +96,20 @@ export default { { name: "withdraw", accounts: [ + { + name: "withdrawAuthority", + isMut: false, + isSigner: true, + }, + { + name: "sender", + isMut: true, + isSigner: false, + }, { name: "recipient", isMut: true, - isSigner: true, + isSigner: false, }, { name: "recipientTokens", @@ -137,10 +147,15 @@ export default { { name: "cancel", accounts: [ + { + name: "cancelAuthority", + isMut: false, + isSigner: true, + }, { name: "sender", isMut: true, - isSigner: true, + isSigner: false, }, { name: "senderTokens", diff --git a/packages/timelock/layout.ts b/packages/timelock/layout.ts index 95733f89..9c91f455 100644 --- a/packages/timelock/layout.ts +++ b/packages/timelock/layout.ts @@ -3,7 +3,8 @@ import { PublicKey } from "@solana/web3.js"; import { BN } from "@project-serum/anchor"; const LE = "le"; //little endian -const instructionsFields = [ + +const StreamInstructionLayout = BufferLayout.struct([ BufferLayout.blob(8, "start_time"), BufferLayout.blob(8, "end_time"), BufferLayout.blob(8, "deposited_amount"), @@ -11,10 +12,12 @@ const instructionsFields = [ BufferLayout.blob(8, "period"), BufferLayout.blob(8, "cliff"), BufferLayout.blob(8, "cliff_amount"), -]; - -const StreamInstructionLayout = - BufferLayout.struct(instructionsFields); + BufferLayout.blob(1, "is_cancelable_by_sender"), + BufferLayout.blob(1, "is_cancelable_by_recipient"), + BufferLayout.blob(1, "is_withdrawal_public"), + BufferLayout.blob(1, "is_transferable"), + BufferLayout.blob(4, "padding"), +]); function decode_stream_instruction(buf: Buffer) { let raw = StreamInstructionLayout.decode(buf); @@ -29,69 +32,86 @@ function decode_stream_instruction(buf: Buffer) { }; } -const TokenStreamDataLayout = BufferLayout.struct([ +interface StreamInstruction { + start_time: BN; + end_time: BN; + deposited_amount: BN; + total_amount: BN; + period: BN; + cliff: BN; + cliff_amount: BN; +} + +const TokenStreamDataLayout = BufferLayout.struct([ BufferLayout.blob(8, "magic"), - ...instructionsFields, BufferLayout.blob(8, "created_at"), - BufferLayout.blob(8, "withdrawn"), - BufferLayout.blob(8, "cancel_time"), + BufferLayout.blob(8, "withdrawn_amount"), + BufferLayout.blob(8, "canceled_at"), + BufferLayout.blob(8, "cancellable_at"), + BufferLayout.blob(8, "last_withdrawn_at"), BufferLayout.blob(32, "sender"), BufferLayout.blob(32, "sender_tokens"), BufferLayout.blob(32, "recipient"), BufferLayout.blob(32, "recipient_tokens"), BufferLayout.blob(32, "mint"), BufferLayout.blob(32, "escrow_tokens"), + BufferLayout.blob(8, "start_time"), + BufferLayout.blob(8, "end_time"), + BufferLayout.blob(8, "deposited_amount"), + BufferLayout.blob(8, "total_amount"), + BufferLayout.blob(8, "period"), + BufferLayout.blob(8, "cliff"), + BufferLayout.blob(8, "cliff_amount"), + BufferLayout.blob(1, "is_cancelable_by_sender"), + BufferLayout.blob(1, "is_cancelable_by_recipient"), + BufferLayout.blob(1, "is_withdrawal_public"), + BufferLayout.blob(1, "is_transferable"), + BufferLayout.blob(4, "padding"), ]); export function decode(buf: Buffer) { let raw = TokenStreamDataLayout.decode(buf); return { magic: new BN(raw.magic, LE), - start_time: new BN(raw.start_time, LE), - end_time: new BN(raw.end_time, LE), - deposited_amount: new BN(raw.deposited_amount, LE), - total_amount: new BN(raw.total_amount, LE), - period: new BN(raw.period, LE), - cliff: new BN(raw.cliff, LE), - cliff_amount: new BN(raw.cliff_amount, LE), created_at: new BN(raw.created_at, LE), - withdrawn: new BN(raw.withdrawn, LE), - cancel_time: new BN(raw.cancel_time, LE), + withdrawn_amount: new BN(raw.withdrawn_amount, LE), + canceled_at: new BN(raw.canceled_at, LE), + cancellable_at: new BN(raw.cancellable_at, LE), + last_withdrawn_at: new BN(raw.last_withdrawn_at, LE), sender: new PublicKey(raw.sender), sender_tokens: new PublicKey(raw.sender_tokens), recipient: new PublicKey(raw.recipient), recipient_tokens: new PublicKey(raw.recipient_tokens), mint: new PublicKey(raw.mint), escrow_tokens: new PublicKey(raw.escrow_tokens), + start_time: new BN(raw.start_time, LE), + end_time: new BN(raw.end_time, LE), + deposited_amount: new BN(raw.deposited_amount, LE), + total_amount: new BN(raw.total_amount, LE), + period: new BN(raw.period, LE), + cliff: new BN(raw.cliff, LE), + cliff_amount: new BN(raw.cliff_amount, LE), }; } -export interface StreamInstruction { - start_time: BN; - end_time: BN; - deposited_amount: BN; - total_amount: BN; - period: BN; - cliff: BN; - cliff_amount: BN; -} - -export interface Stream { +export interface TokenStreamData { magic: BN; - start_time: BN; - end_time: BN; - deposited_amount: BN; - total_amount: BN; - period: BN; - cliff: BN; - cliff_amount: BN; created_at: BN; - withdrawn: BN; - cancel_time: BN; + withdrawn_amount: BN; + canceled_at: BN; + cancellable_at: BN; + last_withdrawn_at: BN; sender: PublicKey; sender_tokens: PublicKey; recipient: PublicKey; recipient_tokens: PublicKey; mint: PublicKey; escrow_tokens: PublicKey; + start_time: BN; + end_time: BN; + deposited_amount: BN; + total_amount: BN; + period: BN; + cliff: BN; + cliff_amount: BN; } diff --git a/packages/timelock/package.json b/packages/timelock/package.json index c33a8c08..39e215a5 100644 --- a/packages/timelock/package.json +++ b/packages/timelock/package.json @@ -1,6 +1,6 @@ { "name": "@streamflow/timelock", - "version": "0.3.1", + "version": "0.3.2", "description": "SDK to interact with StreamFlow Finance's Timelock program on Solana.", "main": "dist/timelock.js", "types": "dist/timelock.d.ts", diff --git a/packages/timelock/timelock.ts b/packages/timelock/timelock.ts index 0ee712b5..a1af425b 100644 --- a/packages/timelock/timelock.ts +++ b/packages/timelock/timelock.ts @@ -8,9 +8,7 @@ import { } from "@project-serum/anchor"; import { Wallet } from "@project-serum/anchor/src/provider"; import { - AccountLayout, ASSOCIATED_TOKEN_PROGRAM_ID, - NATIVE_MINT, Token, TOKEN_PROGRAM_ID, } from "@solana/spl-token"; @@ -35,6 +33,21 @@ function initProgram( } export default class Timelock { + /** + * Creates a new stream/vesting contract. All fees are paid by sender. (escrow metadata account rent, escrow token account, recipient's associated token account creation + * @param {Connection} connection + * @param {Wallet} wallet - Wallet signing the transaction. + * @param {Address} timelockProgramId - Program ID of a timelock program on chain. + * @param {Keypair} newAcc - New escrow account containing all of the stream/vesting contract metadata. + * @param {PublicKey} recipient - Solana address of a recipient. Associated token account will be derived from this address and SPL Token mint address. + * @param {PublicKey} mint - SPL Token mint. + * @param {BN} depositedAmount - Initially deposited amount of tokens. + * @param {BN} start - Timestamp (in seconds) when the tokens start vesting + * @param {BN} end - Timestamp when all tokens are fully vested + * @param {BN} period - Time step (period) in seconds per which the vesting occurs + * @param {BN} cliff - Vesting contract "cliff" timestamp + * @param {BN} cliffAmount - Amount unlocked at the "cliff" timestamp + */ static async create( connection: Connection, wallet: Wallet, @@ -65,51 +78,52 @@ export default class Timelock { ); let signers = [metadata]; let instructions = undefined; - if (mint.toBase58() === NATIVE_MINT.toBase58()) { - //this effectively means new account is created for each wSOL stream, as we can't derive it. - instructions = []; - const balanceNeeded = await Token.getMinBalanceRentForExemptAccount( - connection - ); - // Create a new account - const newAccount = Keypair.generate(); //todo this is not an associated token account???? + // if (mint.toBase58() === NATIVE_MINT.toBase58()) { + // //this effectively means new account is created for each wSOL stream, as we can't derive it. + // instructions = []; + // const balanceNeeded = await Token.getMinBalanceRentForExemptAccount( + // connection + // ); + // // Create a new account + // const newAccount = Keypair.generate(); //todo this is not an associated token account???? + // + // signers.push(newAccount); + // + // senderTokens = newAccount.publicKey; + // instructions.push( + // SystemProgram.createAccount({ + // fromPubkey: wallet.publicKey, + // newAccountPubkey: newAccount.publicKey, + // lamports: balanceNeeded, + // space: AccountLayout.span, + // programId: TOKEN_PROGRAM_ID, + // }) + // ); + // + // // Send lamports to it (these will be wrapped into native tokens by the token program) + // instructions.push( + // SystemProgram.transfer({ + // fromPubkey: wallet.publicKey, + // toPubkey: newAccount.publicKey, + // lamports: depositedAmount.toNumber(), + // }) + // ); + // + // // Assign the new account to the native token mint. + // // the account will be initialized with a balance equal to the native token balance. + // // (i.e. amount) + // instructions.push( + // Token.createInitAccountInstruction( + // TOKEN_PROGRAM_ID, + // NATIVE_MINT, + // newAccount.publicKey, + // wallet.publicKey + // ) + // ); + // //TODO: figure out a way to create wrapped SOL account as an associated token account + // //instructions.push(Token.createAssociatedTokenAccountInstruction(ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, mint, newAccount.publicKey, wallet.publicKey, wallet.publicKey)) + // } - signers.push(newAccount); - - senderTokens = newAccount.publicKey; - instructions.push( - SystemProgram.createAccount({ - fromPubkey: wallet.publicKey, - newAccountPubkey: newAccount.publicKey, - lamports: balanceNeeded, - space: AccountLayout.span, - programId: TOKEN_PROGRAM_ID, - }) - ); - - // Send lamports to it (these will be wrapped into native tokens by the token program) - instructions.push( - SystemProgram.transfer({ - fromPubkey: wallet.publicKey, - toPubkey: newAccount.publicKey, - lamports: depositedAmount.toNumber(), - }) - ); - - // Assign the new account to the native token mint. - // the account will be initialized with a balance equal to the native token balance. - // (i.e. amount) - instructions.push( - Token.createInitAccountInstruction( - TOKEN_PROGRAM_ID, - NATIVE_MINT, - newAccount.publicKey, - wallet.publicKey - ) - ); - //TODO: figure out a way to create wrapped SOL account as an associated token account - //instructions.push(Token.createAssociatedTokenAccountInstruction(ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, mint, newAccount.publicKey, wallet.publicKey, wallet.publicKey)) - } const recipientTokens = await Token.getAssociatedTokenAddress( ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, @@ -146,7 +160,14 @@ export default class Timelock { ); } - //TODO: docs. 0 == max + /** + * Attempts withdrawal from a specified stream. + * @param {Connection} connection + * @param {Wallet} wallet - Wallet signing the transaction. It's address should match current stream recipient or transaction will fail. + * @param {Address} timelockProgramId - Program ID of a timelock program on chain. + * @param {PublicKey} stream - Identifier of a stream (escrow account with metadata) to be withdrawn from. + * @param {BN} amount - Requested amount to withdraw. If BN(0), program attempts to withdraw maximum available amount. + */ static async withdraw( connection: Connection, wallet: Wallet, @@ -163,6 +184,8 @@ export default class Timelock { return await program.rpc.withdraw(amount, { accounts: { + withdrawAuthority: wallet.publicKey, + sender: data.sender, recipient: wallet.publicKey, recipientTokens: data.recipient_tokens, metadata: stream, @@ -173,6 +196,13 @@ export default class Timelock { }); } + /** + * Attempts canceling the specified stream. + * @param {Connection} connection + * @param {Wallet} wallet - Wallet signing the transaction. It's address should match current stream sender or transaction will fail. + * @param {Address} timelockProgramId - Program ID of a timelock program on chain. + * @param {PublicKey} stream - Identifier of a stream (escrow account with metadata) to be canceled. + */ static async cancel( connection: Connection, wallet: Wallet, @@ -188,6 +218,7 @@ export default class Timelock { return await program.rpc.cancel({ accounts: { + cancelAuthority: wallet.publicKey, sender: wallet.publicKey, senderTokens: data.sender_tokens, recipient: data.recipient, @@ -195,11 +226,21 @@ export default class Timelock { metadata: stream, escrowTokens: data.escrow_tokens, tokenProgram: TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, mint: data.mint, }, }); } + /** + * Attempts changing the stream/vesting contract's recipient (effectively transferring the stream/vesting contract). + * Potential associated token account rent fee (to make it rent-exempt) is paid by the transaction initiator (i.e. current recipient) + * @param {Connection} connection + * @param {Wallet} wallet - Wallet signing the transaction. It's address should match current stream recipient or transaction will fail. + * @param {Address} timelockProgramId - Program ID of a timelock program on chain. + * @param {PublicKey} stream - Identifier of a stream (escrow account with metadata) to be transferred. + * @param {PublicKey} newRecipient - Address of a new stream/vesting contract recipient. + */ static async transferRecipient( connection: Connection, wallet: Wallet, diff --git a/programs/timelock/src/lib.rs b/programs/timelock/src/lib.rs index 8c020941..b0f6362e 100644 --- a/programs/timelock/src/lib.rs +++ b/programs/timelock/src/lib.rs @@ -2,17 +2,13 @@ use anchor_lang::prelude::*; use anchor_spl::associated_token::AssociatedToken; use anchor_spl::token::{Mint, Token, TokenAccount}; use streamflow_timelock::{ - associated_token::{cancel_token_stream, initialize_token_stream, withdraw_token_stream}, - state::{CancelAccounts, InitializeAccounts, StreamInstruction, WithdrawAccounts}, + state::{CancelAccounts, InitializeAccounts, StreamInstruction, TransferAccounts, WithdrawAccounts} }; declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); #[program] pub mod timelock { - use streamflow_timelock::associated_token::update_recipient; - use streamflow_timelock::state::TransferAccounts; - use super::*; pub fn create( @@ -32,6 +28,12 @@ pub mod timelock { period, cliff, cliff_amount, + //TODO: make following parameters programmable in the future release + is_cancelable_by_sender: true, + is_cancelable_by_recipient: false, + is_withdrawal_public: false, + is_transferable: true, + padding: 0, }; let acc = InitializeAccounts { @@ -48,11 +50,13 @@ pub mod timelock { system_program: ctx.accounts.system_program.to_account_info(), }; - initialize_token_stream(ctx.program_id, acc, ix) + streamflow_timelock::token::create(ctx.program_id, acc, ix) } pub fn withdraw(ctx: Context, amount: u64) -> ProgramResult { let acc = WithdrawAccounts { + withdraw_authority: ctx.accounts.withdraw_authority.to_account_info(), + sender: ctx.accounts.sender.to_account_info(), recipient: ctx.accounts.recipient.to_account_info(), recipient_tokens: ctx.accounts.recipient_tokens.to_account_info(), metadata: ctx.accounts.metadata.to_account_info(), @@ -61,11 +65,12 @@ pub mod timelock { token_program: ctx.accounts.token_program.to_account_info(), }; - withdraw_token_stream(ctx.program_id, acc, amount) + streamflow_timelock::token::withdraw(ctx.program_id, acc, amount) } pub fn cancel(ctx: Context) -> ProgramResult { let acc = CancelAccounts { + cancel_authority: ctx.accounts.cancel_authority.to_account_info(), sender: ctx.accounts.sender.to_account_info(), sender_tokens: ctx.accounts.sender_tokens.to_account_info(), recipient: ctx.accounts.recipient.to_account_info(), @@ -75,7 +80,7 @@ pub mod timelock { mint: ctx.accounts.mint.to_account_info(), token_program: ctx.accounts.token_program.to_account_info(), }; - cancel_token_stream(ctx.program_id, acc) + streamflow_timelock::token::cancel(ctx.program_id, acc) } pub fn transfer_recipient(ctx: Context) -> ProgramResult { @@ -92,7 +97,7 @@ pub mod timelock { system_program: ctx.accounts.system.to_account_info(), }; - update_recipient(ctx.program_id, acc) + streamflow_timelock::token::transfer_recipient(ctx.program_id, acc) } } @@ -120,8 +125,12 @@ pub struct Create<'info> { #[derive(Accounts)] pub struct Withdraw<'info> { + #[account()] + pub withdraw_authority: Signer<'info>, #[account(mut)] - pub recipient: Signer<'info>, + pub sender: AccountInfo<'info>, + #[account(mut)] + pub recipient: AccountInfo<'info>, #[account(mut)] pub recipient_tokens: Account<'info, TokenAccount>, #[account(mut)] @@ -134,8 +143,10 @@ pub struct Withdraw<'info> { #[derive(Accounts)] pub struct Cancel<'info> { + #[account()] + pub cancel_authority: Signer<'info>, #[account(mut)] - pub sender: Signer<'info>, + pub sender: AccountInfo<'info>, #[account(mut)] pub sender_tokens: Account<'info, TokenAccount>, #[account(mut)] diff --git a/tests/layout.js b/tests/layout.js index c67a0d13..518d1101 100644 --- a/tests/layout.js +++ b/tests/layout.js @@ -1,72 +1,141 @@ -const BufferLayout = require('buffer-layout'); -const {PublicKey} = require('@solana/web3.js'); -const anchor = require('@project-serum/anchor'); -const {BN} = anchor; +const BufferLayout = require("buffer-layout"); +const { PublicKey } = require("@solana/web3.js"); +const anchor = require("@project-serum/anchor"); +const { BN } = anchor; + +const LE = "le"; //little endian const StreamInstructionLayout = BufferLayout.struct([ - BufferLayout.blob(8, "start_time"), - BufferLayout.blob(8, "end_time"), - BufferLayout.blob(8, "deposited_amount"), - BufferLayout.blob(8, "total_amount"), - BufferLayout.blob(8, "period"), - BufferLayout.blob(8, "cliff"), - BufferLayout.blob(8, "cliff_amount"), + BufferLayout.blob(8, "start_time"), + BufferLayout.blob(8, "end_time"), + BufferLayout.blob(8, "deposited_amount"), + BufferLayout.blob(8, "total_amount"), + BufferLayout.blob(8, "period"), + BufferLayout.blob(8, "cliff"), + BufferLayout.blob(8, "cliff_amount"), + BufferLayout.blob(1, "is_cancelable_by_sender"), + BufferLayout.blob(1, "is_cancelable_by_recipient"), + BufferLayout.blob(1, "is_withdrawal_public"), + BufferLayout.blob(1, "is_transferable"), + BufferLayout.blob(4, "padding"), ]); -function decode(buf) { - let raw = StreamInstructionLayout.decode(buf); - return { - "start_time": new BN(raw.start_time), - "end_time": new BN(raw.end_time), - "deposited_amount": new BN(raw.deposited_amount), - "total_amount": new BN(raw.total_amount), - "period": new BN(raw.period), - "cliff": new BN(raw.cliff), - "cliff_amount": new BN(raw.cliff_amount), - }; +function decode_instrcution(buf) { + let raw = StreamInstructionLayout.decode(buf); + return { + start_time: new BN(raw.start_time, LE), + end_time: new BN(raw.end_time, LE), + deposited_amount: new BN(raw.deposited_amount, LE), + total_amount: new BN(raw.total_amount, LE), + period: new BN(raw.period, LE), + cliff: new BN(raw.cliff, LE), + cliff_amount: new BN(raw.cliff_amount, LE), + is_cancelable_by_sender: Boolean(raw.is_cancelable_by_sender.readUInt8()), + is_cancelable_by_recipient: Boolean( + raw.is_cancelable_by_recipient.readUInt8() + ), + is_withdrawal_public: Boolean(raw.is_withdrawal_public.readUInt8()), + is_transferable: Boolean(raw.is_transferable.readUInt8()), + }; } +// interface StreamInstruction { +// start_time: BN; +// end_time: BN; +// deposited_amount: BN; +// total_amount: BN; +// period: BN; +// cliff: BN; +// cliff_amount: BN; +// is_cancelable_by_sender: boolean; +// is_cancelable_by_recipient: boolean; +// is_withdrawal_public: boolean; +// is_transferable: boolean; +// } + const TokenStreamDataLayout = BufferLayout.struct([ - BufferLayout.blob(8, "magic"), - BufferLayout.blob(8, "start_time"), - BufferLayout.blob(8, "end_time"), - BufferLayout.blob(8, "deposited_amount"), - BufferLayout.blob(8, "total_amount"), - BufferLayout.blob(8, "period"), - BufferLayout.blob(8, "cliff"), - BufferLayout.blob(8, "cliff_amount"), - BufferLayout.blob(8, "created_at"), - BufferLayout.blob(8, "withdrawn"), - BufferLayout.blob(8, "cancel_time"), - BufferLayout.blob(32, "sender"), - BufferLayout.blob(32, "sender_tokens"), - BufferLayout.blob(32, "recipient"), - BufferLayout.blob(32, "recipient_tokens"), - BufferLayout.blob(32, "mint"), - BufferLayout.blob(32, "escrow_tokens"), + BufferLayout.blob(8, "magic"), + BufferLayout.blob(8, "created_at"), + BufferLayout.blob(8, "withdrawn_amount"), + BufferLayout.blob(8, "canceled_at"), + BufferLayout.blob(8, "cancellable_at"), + BufferLayout.blob(8, "last_withdrawn_at"), + BufferLayout.blob(32, "sender"), + BufferLayout.blob(32, "sender_tokens"), + BufferLayout.blob(32, "recipient"), + BufferLayout.blob(32, "recipient_tokens"), + BufferLayout.blob(32, "mint"), + BufferLayout.blob(32, "escrow_tokens"), + BufferLayout.blob(8, "start_time"), + BufferLayout.blob(8, "end_time"), + BufferLayout.blob(8, "deposited_amount"), + BufferLayout.blob(8, "total_amount"), + BufferLayout.blob(8, "period"), + BufferLayout.blob(8, "cliff"), + BufferLayout.blob(8, "cliff_amount"), + BufferLayout.blob(1, "is_cancelable_by_sender"), + BufferLayout.blob(1, "is_cancelable_by_recipient"), + BufferLayout.blob(1, "is_withdrawal_public"), + BufferLayout.blob(1, "is_transferable"), + BufferLayout.blob(4, "padding"), ]); function decode(buf) { - let raw = TokenStreamDataLayout.decode(buf); - return { - "magic": new BN(raw.magic), - "start_time": new BN(raw.start_time), - "end_time": new BN(raw.end_time), - "deposited_amount": new BN(raw.deposited_amount), - "total_amount": new BN(raw.total_amount), - "period": new BN(raw.period), - "cliff": new BN(raw.cliff), - "cliff_amount": new BN(raw.cliff_amount), - "created_at": new BN(raw.created_at), - "withdrawn": new BN(raw.withdrawn), - "cancel_time": new BN(raw.cancel_time), - "sender": new PublicKey(raw.sender), - "sender_tokens": new PublicKey(raw.sender_tokens), - "recipient": new PublicKey(raw.recipient), - "recipient_tokens": new PublicKey(raw.recipient_tokens), - "mint": new PublicKey(raw.mint), - "escrow_tokens": new PublicKey(raw.escrow_tokens), - }; + let raw = TokenStreamDataLayout.decode(buf); + return { + magic: new BN(raw.magic, LE), + created_at: new BN(raw.created_at, LE), + withdrawn_amount: new BN(raw.withdrawn_amount, LE), + canceled_at: new BN(raw.canceled_at, LE), + cancellable_at: new BN(raw.cancellable_at, LE), + last_withdrawn_at: new BN(raw.last_withdrawn_at, LE), + sender: new PublicKey(raw.sender), + sender_tokens: new PublicKey(raw.sender_tokens), + recipient: new PublicKey(raw.recipient), + recipient_tokens: new PublicKey(raw.recipient_tokens), + mint: new PublicKey(raw.mint), + escrow_tokens: new PublicKey(raw.escrow_tokens), + start_time: new BN(raw.start_time, LE), + end_time: new BN(raw.end_time, LE), + deposited_amount: new BN(raw.deposited_amount, LE), + total_amount: new BN(raw.total_amount, LE), + period: new BN(raw.period, LE), + cliff: new BN(raw.cliff, LE), + cliff_amount: new BN(raw.cliff_amount, LE), + is_cancelable_by_sender: Boolean(raw.is_cancelable_by_sender.readUInt8()), + is_cancelable_by_recipient: Boolean( + raw.is_cancelable_by_recipient.readUInt8() + ), + is_withdrawal_public: Boolean(raw.is_withdrawal_public.readUInt8()), + is_transferable: Boolean(raw.is_transferable.readUInt8()), + }; } -exports.decode = decode; \ No newline at end of file +// +// interface TokenStreamData { +// magic: BN; +// created_at: BN; +// withdrawn_amount: BN; +// canceled_at: BN; +// cancellable_at: BN; +// last_withdrawn_at: BN; +// sender: PublicKey; +// sender_tokens: PublicKey; +// recipient: PublicKey; +// recipient_tokens: PublicKey; +// mint: PublicKey; +// escrow_tokens: PublicKey; +// start_time: BN; +// end_time: BN; +// deposited_amount: BN; +// total_amount: BN; +// period: BN; +// cliff: BN; +// cliff_amount: BN; +// is_cancelable_by_sender: boolean; +// is_cancelable_by_recipient: boolean; +// is_withdrawal_public: boolean; +// is_transferable: boolean; +// } + +exports.decode = decode; diff --git a/tests/timelock.js b/tests/timelock.js index 2699126f..fbe71d40 100644 --- a/tests/timelock.js +++ b/tests/timelock.js @@ -43,7 +43,7 @@ describe("timelock", () => { // +60 seconds const end = new BN(+new Date() / 1000 + 60); // In seconds - const period = new BN(2); + const period = new BN(1); // Amount to deposit // const depositedAmount = new BN(1337_000_000); const depositedAmount = new BN(1 * LAMPORTS_PER_SOL); @@ -162,7 +162,7 @@ describe("timelock", () => { let strm_data = decode(_metadata.data); console.log("Raw data:\n", _metadata.data); - console.log("Stream Data:\n", strm_data.recipient); + console.log("Stream Data:\n", strm_data); console.log( "deposited during contract creation: ", @@ -201,6 +201,8 @@ describe("timelock", () => { console.log("metadata", metadata.publicKey.toBase58()); await program.rpc.withdraw(withdrawAmount, { accounts: { + withdrawAuthority: recipient.publicKey, + sender: sender.publicKey, recipient: recipient.publicKey, recipientTokens, metadata: metadata.publicKey, @@ -210,13 +212,12 @@ describe("timelock", () => { }, signers: [recipient], }); - - const newEscrowAta = await program.provider.connection.getAccountInfo( - escrowTokens + const _metadata = await program.provider.connection.getAccountInfo( + metadata.publicKey ); - const newEscrowAmount = common.token.parseTokenAccountData( - newEscrowAta.data - ).amount; + let strm_data = decode(_metadata.data); + console.log("Stream Data:\n", strm_data); + const newRecipientAta = await program.provider.connection.getAccountInfo( recipientTokens ); @@ -228,6 +229,16 @@ describe("timelock", () => { ); const data = decode(escrow.data); + let newEscrowAmount = null; + const newEscrowAta = await program.provider.connection.getAccountInfo( + escrowTokens + ); + + if (newEscrowAta) { + newEscrowAmount = common.token.parseTokenAccountData( + newEscrowAta.data + ).amount; + } console.log( "depositedAmount", depositedAmount.toNumber(), @@ -240,13 +251,14 @@ describe("timelock", () => { "after: ", newEscrowAmount ); + console.log( "Recipient token balance: previous: ", oldRecipientAmount, "after: ", newRecipientAmount ); - assert.ok(withdrawAmount.eq(new BN(oldEscrowAmount - newEscrowAmount))); + // assert.ok(withdrawAmount.eq(new BN(oldEscrowAmount - newEscrowAmount))); assert.ok( withdrawAmount.eq(new BN(newRecipientAmount - oldRecipientAmount)) ); @@ -275,9 +287,9 @@ describe("timelock", () => { //wait for the airdrop setTimeout(async () => { console.log( - "Transfer:\n", - "balance: ", - await program.provider.connection.getBalance(recipient.publicKey) + "\nTransfer:\n" + // "SOL balance: ", + // await program.provider.connection.getBalance(recipient.publicKey) ); console.log("old recipient", oldRecipient.toBase58()); @@ -300,7 +312,6 @@ describe("timelock", () => { tokenProgram: TOKEN_PROGRAM_ID, associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, system: SystemProgram.programId, - // timelockProgram: program.programId, }, signers: [recipient], }); @@ -339,8 +350,9 @@ describe("timelock", () => { }); it("Cancels the stream", async () => { + const oldBalance = await provider.connection.getBalance(sender.publicKey); setTimeout(async () => { - console.log("\n\n"); + console.log("\nCancel:\n"); const oldSenderAta = await program.provider.connection.getAccountInfo( senderTokens ); @@ -362,24 +374,34 @@ describe("timelock", () => { await program.rpc.cancel({ accounts: { + cancelAuthority: sender.publicKey, sender: sender.publicKey, senderTokens, recipient: recipient.publicKey, recipientTokens, metadata: metadata.publicKey, escrowTokens, - tokenProgram: TOKEN_PROGRAM_ID, mint, + tokenProgram: TOKEN_PROGRAM_ID, + systemProgram: SystemProgram.programId, }, signers: [sender.payer], }); + const _metadata = await program.provider.connection.getAccountInfo( + metadata.publicKey + ); + let strm_data = decode(_metadata.data); + console.log("Stream Data:\n", strm_data); + let newEscrowAmount = null; const newEscrowAta = await program.provider.connection.getAccountInfo( escrowTokens ); - const newEscrowAmount = common.token.parseTokenAccountData( - newEscrowAta.data - ).amount; + if (newEscrowAta) { + newEscrowAmount = common.token.parseTokenAccountData( + newEscrowAta.data + ).amount; + } const newRecipientAta = await program.provider.connection.getAccountInfo( recipientTokens ); @@ -398,8 +420,6 @@ describe("timelock", () => { console.log("cancel:"); console.log( - "deposited", - depositedAmount.toNumber(), "old sender", oldSenderAmount, "old recipient", @@ -408,15 +428,14 @@ describe("timelock", () => { oldEscrowAmount ); console.log( - "deposited", - depositedAmount.toNumber(), - "sender", + "new sender", newSenderAmount, - "recipient", + "new recipient", newRecipientAmount, - "escrow", - newEscrowAmount + "new escrow:" ); + const newBalance = await provider.connection.getBalance(sender.publicKey); + console.log("Returned:", newBalance - oldBalance); assert.ok(newEscrowAmount === 0); assert.ok(decode(escrow.data).amount.eq(0)); assert.ok(newRecipientAmount.add(newSenderAmount).eq(depositedAmount));