// @flow import fs from 'mz/fs'; import {Account, Connection, BpfLoader, PublicKey} from '@solana/web3.js'; import semver from 'semver'; import {Token, TokenAmount} from '../client/token'; import {url} from '../url'; import {newAccountWithLamports} from '../client/util/new-account-with-lamports'; import {sleep} from '../client/util/sleep'; import {Store} from '../client/util/store'; // Loaded token program's program id let programId: PublicKey; // A token created by the next test and used by all subsequent tests let mintOwner: Account; let testToken: Token; // Initial token account let testAccountOwner: Account; let testAccount: PublicKey; // A mintable token used by multiple tests let mintableOwner: Account; let testMintableToken: Token; // Initial token account let testMintableAccountOwner: Account; let testMintableAccount: PublicKey; function assert(condition, message) { if (!condition) { console.log(Error().stack + ':token-test.js'); throw message || 'Assertion failed'; } } async function didThrow(func, args): Promise { try { await func.apply(args); } catch (e) { return true; } return false; } let connection; async function getConnection(): Promise { if (connection) return connection; let newConnection = new Connection(url, 'recent', ); const version = await newConnection.getVersion(); // commitment params are only supported >= 0.21.0 const solanaCoreVersion = version['solana-core'].split(' ')[0]; if (semver.gte(solanaCoreVersion, '0.21.0')) { newConnection = new Connection(url, 'recent'); } // eslint-disable-next-line require-atomic-updates connection = newConnection; console.log('Connection to cluster established:', url, version); return connection; } async function loadProgram(connection: Connection, path: string): Promise { const NUM_RETRIES = 500; /* allow some number of retries */ const data = await fs.readFile(path ); const { feeCalculator } = await connection.getRecentBlockhash(); const balanceNeeded = feeCalculator.lamportsPerSignature * (BpfLoader.getMinNumSignatures(data.length) + NUM_RETRIES) + (await connection.getMinimumBalanceForRentExemption(data.length)); const from = await newAccountWithLamports(connection, balanceNeeded); const program_account = new Account(); console.log('Loading program:', path); await BpfLoader.load(connection, from, program_account, data); return program_account.publicKey; } async function GetPrograms(connection: Connection): Promise { const store = new Store(); let tokenProgramId = null; try { const config = await store.load('config.json'); console.log('Using pre-loaded Token program'); console.log(' Note: To reload program remove client/util/store/config.json'); tokenProgramId = new PublicKey(config.tokenProgramId); } catch (err) { tokenProgramId = await loadProgram(connection, '../target/bpfel-unknown-unknown/release/spl_token.so'); await store.save('config.json', { tokenProgramId: tokenProgramId.toString(), }); } return tokenProgramId; } export async function loadTokenProgram(): Promise { const connection = await getConnection(); programId = await GetPrograms(connection); console.log('Token Program ID', programId.toString()); } export async function createMint(): Promise { const connection = await getConnection(); const payer = await newAccountWithLamports(connection, 100000000000 /* wag */); mintOwner = new Account(); testAccountOwner = new Account(); [testToken, testAccount] = await Token.createMint( connection, payer, mintOwner.publicKey, testAccountOwner.publicKey, new TokenAmount(10000), 2, programId, false, ); const mintInfo = await testToken.getMintInfo(); assert(mintInfo.decimals == 2); assert(mintInfo.owner == null); const accountInfo = await testToken.getAccountInfo(testAccount); assert(accountInfo.mint.equals(testToken.publicKey)); assert(accountInfo.owner.equals(testAccountOwner.publicKey)); assert(accountInfo.amount.toNumber() == 10000); assert(accountInfo.delegate == null); assert(accountInfo.delegatedAmount.toNumber() == 0); } export async function createAccount(): Promise { const destOwner = new Account(); const account = await testToken.createAccount(destOwner.publicKey); const accountInfo = await testToken.getAccountInfo(account); assert(accountInfo.mint.equals(testToken.publicKey)); assert(accountInfo.owner.equals(destOwner.publicKey)); assert(accountInfo.amount.toNumber() == 0); assert(accountInfo.delegate == null); } export async function transfer(): Promise { const destOwner = new Account(); const dest = await testToken.createAccount(destOwner.publicKey); await testToken.transfer(testAccount, dest, testAccountOwner, [], 123); await sleep(500); let destAccountInfo = await testToken.getAccountInfo(dest); assert(destAccountInfo.amount.toNumber() == 123); } export async function approveRevoke(): Promise { if (programId == null) { console.log('test skipped, requires "load token program" to succeed'); return; } const delegate = new PublicKey(); await testToken.approve( testAccount, delegate, testAccountOwner, [], 456, ); let testAccountInfo = await testToken.getAccountInfo(testAccount); assert(testAccountInfo.delegatedAmount.toNumber() == 456); if (testAccountInfo.delegate === null) { throw new Error('deleage should not be null'); } else { assert(testAccountInfo.delegate.equals(delegate)); } await testToken.revoke(testAccount, testAccountOwner, []); testAccountInfo = await testToken.getAccountInfo(testAccount); assert(testAccountInfo.delegatedAmount.toNumber() == 0); if (testAccountInfo.delegate != null) { throw new Error('delegate should be null'); } } export async function invalidApprove(): Promise { const owner = new Account(); const account1 = await testToken.createAccount(owner.publicKey); const account2 = await testToken.createAccount(owner.publicKey); const delegate = new Account(); // account2 is not a delegate account of account1 assert(didThrow(testToken.approve, [account1, account2, owner, [], 123])); // account1Delegate is not a delegate account of account2 assert(didThrow(testToken.approve, [account2, delegate, owner, [], 123])); } export async function failOnApproveOverspend(): Promise { const owner = new Account(); const account1 = await testToken.createAccount(owner.publicKey); const account2 = await testToken.createAccount(owner.publicKey); const delegate = new Account(); await testToken.transfer( testAccount, account1, testAccountOwner, [], 10, ); await testToken.approve(account1, delegate.publicKey, owner, [], 2); let account1Info = await testToken.getAccountInfo(account1); assert(account1Info.amount.toNumber() == 10); assert(account1Info.delegatedAmount.toNumber() == 2); if (account1Info.delegate === null) { throw new Error('deleage should not be null'); } else { assert(account1Info.delegate.equals(delegate.publicKey)); } await testToken.transfer(account1, account2, delegate, [], 1); account1Info = await testToken.getAccountInfo(account1); assert(account1Info.amount.toNumber() == 9); assert(account1Info.delegatedAmount.toNumber() == 1); await testToken.transfer(account1, account2, delegate, [], 1); account1Info = await testToken.getAccountInfo(account1); assert(account1Info.amount.toNumber() == 8); assert(account1Info.delegate === null); assert(account1Info.delegatedAmount.toNumber() == 0); assert(didThrow(testToken.transfer, [account1, account2, delegate, [], 1])); } export async function setOwner(): Promise { const owner = new Account(); const newOwner = new Account(); const owned = await testToken.createAccount(owner.publicKey); await testToken.setOwner(owned, newOwner.publicKey, owner, []); assert(didThrow(testToken.setOwner, [owned, newOwner.publicKey, owner, []])); await testToken.setOwner(owned, owner.publicKey,newOwner, []); } export async function mintTo(): Promise { const connection = await getConnection(); const payer = await newAccountWithLamports(connection, 100000000000 /* wag */); mintableOwner = new Account(); testMintableAccountOwner = new Account(); [testMintableToken, testMintableAccount] = await Token.createMint( connection, payer, mintableOwner.publicKey, testMintableAccountOwner.publicKey, new TokenAmount(10000), 2, programId, true, ); { const mintInfo = await testMintableToken.getMintInfo(); assert(mintInfo.decimals == 2); if (mintInfo.owner === null) { throw new Error('owner should not be null'); } else { assert(mintInfo.owner.equals(mintableOwner.publicKey)); } const accountInfo = await testMintableToken.getAccountInfo(testMintableAccount); assert(accountInfo.mint.equals(testMintableToken.publicKey)); assert(accountInfo.owner.equals(testMintableAccountOwner.publicKey)); assert(accountInfo.amount.toNumber() == 10000); assert(accountInfo.delegate == null); assert(accountInfo.delegatedAmount.toNumber() == 0); } const dest = await testMintableToken.createAccount(testMintableAccountOwner.publicKey); await testMintableToken.mintTo(dest, mintableOwner, [], 42); { const mintInfo = await testMintableToken.getMintInfo(); assert(mintInfo.decimals == 2); if (mintInfo.owner === null) { throw new Error('owner should not be null'); } else { assert(mintInfo.owner.equals(mintableOwner.publicKey)); } const accountInfo = await testMintableToken.getAccountInfo(dest); assert(accountInfo.mint.equals(testMintableToken.publicKey)); assert(accountInfo.owner.equals(testMintableAccountOwner.publicKey)); assert(accountInfo.amount.toNumber() == 42); assert(accountInfo.delegate == null); assert(accountInfo.delegatedAmount.toNumber() == 0); } } export async function burn(): Promise { let accountInfo = await testToken.getAccountInfo(testAccount); const amount = accountInfo.amount.toNumber(); await testToken.burn(testAccount, testAccountOwner, [], 1); await sleep(500); accountInfo = await testToken.getAccountInfo(testAccount); assert(accountInfo.amount.toNumber() == amount - 1); } export async function multisig(): Promise { const m = 2; const n = 5; let signerAccounts = []; for (var i = 0; i < n; i++) { signerAccounts.push(new Account()); } let signerPublicKeys = []; signerAccounts.forEach(account => signerPublicKeys.push(account.publicKey)); const multisig = await testToken.createMultisig(m, signerPublicKeys); const multisigInfo = await testToken.getMultisigInfo(multisig); assert(multisigInfo.m === m); assert(multisigInfo.n === n); assert(multisigInfo.signer1.equals(signerPublicKeys[0])); assert(multisigInfo.signer2.equals(signerPublicKeys[1])); assert(multisigInfo.signer3.equals(signerPublicKeys[2])); assert(multisigInfo.signer4.equals(signerPublicKeys[3])); assert(multisigInfo.signer5.equals(signerPublicKeys[4])); const multisigOwnedAccount = await testToken.createAccount(multisig); const finalDest = await testToken.createAccount(multisig); await testToken.transfer(testAccount, multisigOwnedAccount, testAccountOwner, [], 2); // Transfer via multisig await testToken.transfer(multisigOwnedAccount, finalDest, multisig, signerAccounts, 1); await sleep(500); let accountInfo = await testToken.getAccountInfo(finalDest); assert(accountInfo.amount.toNumber() == 1); // Approve via multisig { const delegate = new PublicKey(); await testToken.approve( multisigOwnedAccount, delegate, multisig, signerAccounts, 1, ); const accountInfo = await testToken.getAccountInfo(multisigOwnedAccount); assert(accountInfo.delegate != null); if (accountInfo.delegate != null) { assert(accountInfo.delegate.equals(delegate)); assert(accountInfo.delegatedAmount.toNumber() == 1); } } // MintTo via multisig { let accountInfo = await testMintableToken.getAccountInfo(testMintableAccount); const initialAmount = accountInfo.amount.toNumber(); await testMintableToken.setOwner(testMintableToken.publicKey, multisig, mintableOwner, []); await testMintableToken.mintTo(testMintableAccount, multisig, signerAccounts, 42); accountInfo = await testMintableToken.getAccountInfo(testMintableAccount); assert(accountInfo.amount.toNumber() == initialAmount + 42); } // SetOwner of mint via multisig { await testMintableToken.setOwner(testMintableToken.publicKey, mintableOwner.publicKey, multisig, signerAccounts); const mintInfo = await testMintableToken.getMintInfo(); assert(mintInfo.owner != null); if (mintInfo.owner != null) { assert(mintInfo.owner.equals(mintableOwner.publicKey)); } } // SetOwner of account via multisig { const newOwner = new PublicKey(); await testToken.setOwner(multisigOwnedAccount, newOwner, multisig, signerAccounts); const accountInfo = await testToken.getAccountInfo(multisigOwnedAccount); assert(accountInfo.owner.equals(newOwner)); } } export async function closeAccount(): Promise { const connection = await getConnection(); const owner = new Account(); const close = await testToken.createAccount(owner.publicKey); let close_balance; let info = await connection.getAccountInfo(close); if (info != null) { close_balance = info.lamports; } else { throw new Error('Account not found'); } const balanceNeeded = await connection.getMinimumBalanceForRentExemption(0); const dest = await newAccountWithLamports(connection, balanceNeeded); info = await connection.getAccountInfo(dest.publicKey); if (info != null) { assert(info.lamports == balanceNeeded); } else { throw new Error('Account not found'); } await testToken.closeAccount(close, dest.publicKey, owner, []); info = await connection.getAccountInfo(close); if (info != null) { throw new Error('Account not closed'); } info = await connection.getAccountInfo(dest.publicKey); if (info != null) { assert(info.lamports == balanceNeeded + close_balance); } else { throw new Error('Account not found'); } } export async function nativeToken(): Promise { const connection = await getConnection(); const mintPublicKey = new PublicKey('So11111111111111111111111111111111111111111'); const payer = await newAccountWithLamports(connection, 100000000000 /* wag */); const token = new Token(connection, mintPublicKey, programId, payer); const owner = new Account(); const native = await token.createAccount(owner.publicKey); let accountInfo = await token.getAccountInfo(native); assert(accountInfo.isNative); let balance; let info = await connection.getAccountInfo(native); if (info != null) { balance = info.lamports; } else { throw new Error('Account not found'); } const balanceNeeded = await connection.getMinimumBalanceForRentExemption(0); const dest = await newAccountWithLamports(connection, balanceNeeded); await token.closeAccount(native, dest.publicKey, owner, []); info = await connection.getAccountInfo(native); if (info != null) { throw new Error('Account not burned'); } info = await connection.getAccountInfo(dest.publicKey); if (info != null) { assert(info.lamports == balanceNeeded + balance); } else { throw new Error('Account not found'); } }