diff --git a/.github/workflows/pull-request-token.yml b/.github/workflows/pull-request-token.yml index 679d17fb426..073eab31596 100644 --- a/.github/workflows/pull-request-token.yml +++ b/.github/workflows/pull-request-token.yml @@ -145,6 +145,13 @@ jobs: - name: Build and test transfer hook example run: ./ci/cargo-test-sbf.sh token/transfer-hook-example + - name: Upload program + uses: actions/upload-artifact@v2 + with: + name: spl-transfer-hook-example + path: "target/deploy/*.so" + if-no-files-found: error + cargo-test-sbf-associated-token-account: runs-on: ubuntu-latest steps: @@ -266,6 +273,11 @@ jobs: with: name: associated-token-account-program path: target/deploy + - name: Download spl-transfer-hook-example program + uses: actions/download-artifact@v2 + with: + name: spl-transfer-hook-example + path: target/deploy - run: ./ci/js-test-token.sh cargo-build-test-cli: diff --git a/token/js/package.json b/token/js/package.json index aafa72be779..181b6304ec8 100644 --- a/token/js/package.json +++ b/token/js/package.json @@ -42,7 +42,7 @@ "test": "npm run test:unit && npm run test:e2e-built && npm run test:e2e-native && npm run test:e2e-2022", "test:unit": "mocha test/unit", "test:e2e-built": "start-server-and-test 'solana-test-validator --bpf-program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA ../../target/deploy/spl_token.so --reset --quiet' http://127.0.0.1:8899/health 'mocha test/e2e'", - "test:e2e-2022": "TEST_PROGRAM_ID=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb start-server-and-test 'solana-test-validator --bpf-program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL ../../target/deploy/spl_associated_token_account.so --bpf-program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb ../../target/deploy/spl_token_2022.so --reset --quiet' http://127.0.0.1:8899/health 'mocha test/e2e*'", + "test:e2e-2022": "TEST_PROGRAM_ID=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb start-server-and-test 'solana-test-validator --bpf-program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL ../../target/deploy/spl_associated_token_account.so --bpf-program TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb ../../target/deploy/spl_token_2022.so --bpf-program TokenHookExampLe8smaVNrxTBezWTRbEwxwb1Zykrb ../../target/deploy/spl_transfer_hook_example.so --reset --quiet' http://127.0.0.1:8899/health 'mocha test/e2e*'", "test:e2e-native": "start-server-and-test 'solana-test-validator --reset --quiet' http://127.0.0.1:8899/health 'mocha test/e2e'", "test:build-programs": "cargo build-sbf --manifest-path ../program/Cargo.toml && cargo build-sbf --manifest-path ../program-2022/Cargo.toml && cargo build-sbf --manifest-path ../../associated-token-account/program/Cargo.toml", "deploy": "npm run deploy:docs", diff --git a/token/js/test/common.ts b/token/js/test/common.ts index a8cf5ed2f29..6f1e20a6fe1 100644 --- a/token/js/test/common.ts +++ b/token/js/test/common.ts @@ -19,3 +19,5 @@ export async function getConnection(): Promise { export const TEST_PROGRAM_ID = process.env.TEST_PROGRAM_ID ? new PublicKey(process.env.TEST_PROGRAM_ID) : TOKEN_PROGRAM_ID; + +export const TRANSFER_HOOK_TEST_PROGRAM_ID = new PublicKey('TokenHookExampLe8smaVNrxTBezWTRbEwxwb1Zykrb'); diff --git a/token/js/test/e2e-2022/transferHook.test.ts b/token/js/test/e2e-2022/transferHook.test.ts index f9a5520c51f..c51605afd77 100644 --- a/token/js/test/e2e-2022/transferHook.test.ts +++ b/token/js/test/e2e-2022/transferHook.test.ts @@ -2,8 +2,8 @@ import chai, { expect } from 'chai'; import chaiAsPromised from 'chai-as-promised'; chai.use(chaiAsPromised); -import type { Connection, Signer } from '@solana/web3.js'; -import { PublicKey } from '@solana/web3.js'; +import type { AccountMeta, Connection, Signer } from '@solana/web3.js'; +import { PublicKey, TransactionInstruction } from '@solana/web3.js'; import { sendAndConfirmTransaction, Keypair, SystemProgram, Transaction } from '@solana/web3.js'; import { createInitializeMintInstruction, @@ -15,31 +15,56 @@ import { updateTransferHook, AuthorityType, setAuthority, + createAssociatedTokenAccountInstruction, + getAssociatedTokenAddressSync, + ASSOCIATED_TOKEN_PROGRAM_ID, + createMintToCheckedInstruction, + getExtraAccountMetaAccount, + ExtraAccountMetaListLayout, + ExtraAccountMetaLayout, + transferCheckedWithTransferHook, + createAssociatedTokenAccountIdempotent, } from '../../src'; -import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; +import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection, TRANSFER_HOOK_TEST_PROGRAM_ID } from '../common'; +import { createHash } from 'crypto'; const TEST_TOKEN_DECIMALS = 2; const EXTENSIONS = [ExtensionType.TransferHook]; describe('transferHook', () => { let connection: Connection; let payer: Signer; + let payerAta: PublicKey; + let destinationAuthority: PublicKey; + let destinationAta: PublicKey; let transferHookAuthority: Keypair; + let pdaExtraAccountMeta: PublicKey; let mint: PublicKey; - let transferHookProgramId: PublicKey; - let newTransferHookProgramId: PublicKey; before(async () => { connection = await getConnection(); payer = await newAccountWithLamports(connection, 1000000000); + destinationAuthority = Keypair.generate().publicKey; transferHookAuthority = Keypair.generate(); - transferHookProgramId = Keypair.generate().publicKey; - newTransferHookProgramId = Keypair.generate().publicKey; }); beforeEach(async () => { const mintKeypair = Keypair.generate(); mint = mintKeypair.publicKey; + pdaExtraAccountMeta = getExtraAccountMetaAccount(TRANSFER_HOOK_TEST_PROGRAM_ID, mint); + payerAta = getAssociatedTokenAddressSync( + mint, + payer.publicKey, + false, + TEST_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + destinationAta = getAssociatedTokenAddressSync( + mint, + destinationAuthority, + false, + TEST_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); const mintLen = getMintLen(EXTENSIONS); const lamports = await connection.getMinimumBalanceForRentExemption(mintLen); - const transaction = new Transaction().add( SystemProgram.createAccount({ fromPubkey: payer.publicKey, @@ -51,7 +76,7 @@ describe('transferHook', () => { createInitializeTransferHookInstruction( mint, transferHookAuthority.publicKey, - transferHookProgramId, + TRANSFER_HOOK_TEST_PROGRAM_ID, TEST_PROGRAM_ID ), createInitializeMintInstruction(mint, TEST_TOKEN_DECIMALS, payer.publicKey, null, TEST_PROGRAM_ID) @@ -65,10 +90,11 @@ describe('transferHook', () => { expect(transferHook).to.not.be.null; if (transferHook !== null) { expect(transferHook.authority).to.eql(transferHookAuthority.publicKey); - expect(transferHook.programId).to.eql(transferHookProgramId); + expect(transferHook.programId).to.eql(TRANSFER_HOOK_TEST_PROGRAM_ID); } }); it('can be updated', async () => { + const newTransferHookProgramId = Keypair.generate().publicKey; await updateTransferHook( connection, payer, @@ -106,4 +132,93 @@ describe('transferHook', () => { expect(transferHook.authority).to.eql(PublicKey.default); } }); + it('transferChecked', async () => { + const extraAccount = Keypair.generate().publicKey; + const keys: AccountMeta[] = [ + { pubkey: pdaExtraAccountMeta, isSigner: false, isWritable: true }, + { pubkey: mint, isSigner: false, isWritable: false }, + { pubkey: payer.publicKey, isSigner: true, isWritable: false }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + ]; + + const data = Buffer.alloc(8 + 4 + ExtraAccountMetaLayout.span); + const discriminator = createHash('sha256') + .update('spl-transfer-hook-interface:initialize-extra-account-metas') + .digest() + .subarray(0, 8); + discriminator.copy(data); + ExtraAccountMetaListLayout.encode( + { + count: 1, + extraAccounts: [ + { + discriminator: 0, + addressConfig: extraAccount.toBuffer(), + isSigner: false, + isWritable: false, + }, + ], + }, + data, + 8 + ); + + const initExtraAccountMetaInstruction = new TransactionInstruction({ + keys, + data, + programId: TRANSFER_HOOK_TEST_PROGRAM_ID, + }); + + const setupTransaction = new Transaction().add( + initExtraAccountMetaInstruction, + SystemProgram.transfer({ + fromPubkey: payer.publicKey, + toPubkey: pdaExtraAccountMeta, + lamports: 10000000, + }), + createAssociatedTokenAccountInstruction( + payer.publicKey, + payerAta, + payer.publicKey, + mint, + TEST_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ), + createMintToCheckedInstruction( + mint, + payerAta, + payer.publicKey, + 5 * 10 ** TEST_TOKEN_DECIMALS, + TEST_TOKEN_DECIMALS, + [], + TEST_PROGRAM_ID + ) + ); + + await sendAndConfirmTransaction(connection, setupTransaction, [payer]); + + await createAssociatedTokenAccountIdempotent( + connection, + payer, + mint, + destinationAuthority, + undefined, + TEST_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + + await transferCheckedWithTransferHook( + connection, + payer, + payerAta, + mint, + destinationAta, + payer, + BigInt(10 ** TEST_TOKEN_DECIMALS), + TEST_TOKEN_DECIMALS, + [], + undefined, + TEST_PROGRAM_ID + ); + }); }); diff --git a/token/js/test/unit/transferHook.test.ts b/token/js/test/unit/transferHook.test.ts index e90f1275ee2..7d4909b8125 100644 --- a/token/js/test/unit/transferHook.test.ts +++ b/token/js/test/unit/transferHook.test.ts @@ -1,6 +1,6 @@ import { getExtraAccountMetas, resolveExtraAccountMeta } from '../../src'; import { expect } from 'chai'; -import { AccountMeta, Keypair, PublicKey } from '@solana/web3.js'; +import { PublicKey } from '@solana/web3.js'; describe('transferHookExtraAccounts', () => { const testProgramId = new PublicKey('7N4HggYEJAtCLJdnHGCtFqfxcB5rhQCsQTze3ftYstVj'); @@ -70,7 +70,7 @@ describe('transferHookExtraAccounts', () => { const extraAccountList = Buffer.concat([ Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), // u64 accountDiscriminator - Buffer.from([0, 0, 0, 0]), // u32 arrayDiscriminator + Buffer.from([0, 0, 0, 0]), // u32 length Buffer.from([3, 0, 0, 0]), // u32 count plainExtraAccount, pdaExtraAccount,