From 5fb20b441d473071a9385a8c364453c1f2a7ddf3 Mon Sep 17 00:00:00 2001 From: Will <82029448+wjthieme@users.noreply.github.com> Date: Mon, 8 Jan 2024 08:58:00 +0100 Subject: [PATCH] Added a WrappedSolAccountCreateMethod (ata) using syncNative (#66) * Added a WrappedSolAccountCreateMethod (ata) using syncNative * Version bump * Create ata instruction * Cleanup * Set default back to keypair * Amend * Update token-util.ts --- packages/common-sdk/package.json | 2 +- packages/common-sdk/src/web3/ata-util.ts | 2 +- packages/common-sdk/src/web3/token-util.ts | 83 +++++++++++++++++-- .../common-sdk/tests/web3/ata-util.test.ts | 63 +++++++++++++- 4 files changed, 140 insertions(+), 10 deletions(-) diff --git a/packages/common-sdk/package.json b/packages/common-sdk/package.json index 3c99233..7414690 100644 --- a/packages/common-sdk/package.json +++ b/packages/common-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@orca-so/common-sdk", - "version": "0.3.5", + "version": "0.3.6", "description": "Common Typescript components across Orca", "repository": "https://github.com/orca-so/orca-sdks", "author": "Orca Foundation", diff --git a/packages/common-sdk/src/web3/ata-util.ts b/packages/common-sdk/src/web3/ata-util.ts index f8358de..d9be1e1 100644 --- a/packages/common-sdk/src/web3/ata-util.ts +++ b/packages/common-sdk/src/web3/ata-util.ts @@ -143,7 +143,7 @@ export async function resolveOrCreateATAs( ownerAddress, wrappedSolAmountIn, accountRentExempt, - undefined, // use default + payer, undefined, // use default wrappedSolAccountCreateMethod ); diff --git a/packages/common-sdk/src/web3/token-util.ts b/packages/common-sdk/src/web3/token-util.ts index be333f1..e9f180a 100644 --- a/packages/common-sdk/src/web3/token-util.ts +++ b/packages/common-sdk/src/web3/token-util.ts @@ -2,12 +2,14 @@ import { AccountLayout, NATIVE_MINT, TOKEN_PROGRAM_ID, + createAssociatedTokenAccountIdempotentInstruction, createCloseAccountInstruction, createInitializeAccountInstruction, + createSyncNativeInstruction, createTransferCheckedInstruction, getAssociatedTokenAddressSync, } from "@solana/spl-token"; -import { Connection, Keypair, PublicKey, SystemProgram } from "@solana/web3.js"; +import { Connection, Keypair, PublicKey, SystemProgram, TransactionInstruction } from "@solana/web3.js"; import { sha256 } from '@noble/hashes/sha256'; import BN from "bn.js"; import invariant from "tiny-invariant"; @@ -23,7 +25,7 @@ export type ResolvedTokenAddressInstruction = { /** * @category Util */ -export type WrappedSolAccountCreateMethod = "keypair" | "withSeed"; +export type WrappedSolAccountCreateMethod = "keypair" | "withSeed" | "ata"; /** * @category Util @@ -52,11 +54,36 @@ export class TokenUtil { createAccountMethod: WrappedSolAccountCreateMethod = "keypair", ): ResolvedTokenAddressInstruction { const payerKey = payer ?? owner; - const unwrapDestinationKey = unwrapDestination ?? payer ?? owner; + const unwrapDestinationKey = unwrapDestination ?? owner; - return createAccountMethod === "keypair" - ? createWrappedNativeAccountInstructionWithKeypair(owner, amountIn, rentExemptLamports, payerKey, unwrapDestinationKey) - : createWrappedNativeAccountInstructionWithSeed(owner, amountIn, rentExemptLamports, payerKey, unwrapDestinationKey); + switch (createAccountMethod) { + case "ata": + return createWrappedNativeAccountInstructionWithATA( + owner, + amountIn, + rentExemptLamports, + payerKey, + unwrapDestinationKey + ); + case "keypair": + return createWrappedNativeAccountInstructionWithKeypair( + owner, + amountIn, + rentExemptLamports, + payerKey, + unwrapDestinationKey + ); + case "withSeed": + return createWrappedNativeAccountInstructionWithSeed( + owner, + amountIn, + rentExemptLamports, + payerKey, + unwrapDestinationKey + ); + default: + throw new Error(`Invalid createAccountMethod: ${createAccountMethod}`); + } } /** @@ -129,6 +156,50 @@ export class TokenUtil { } } +function createWrappedNativeAccountInstructionWithATA( + owner: PublicKey, + amountIn: BN, + _rentExemptLamports: number, + payerKey: PublicKey, + unwrapDestinationKey: PublicKey, +): ResolvedTokenAddressInstruction { + const tempAccount = getAssociatedTokenAddressSync(NATIVE_MINT, owner); + + const instructions: TransactionInstruction[] = [ + createAssociatedTokenAccountIdempotentInstruction( + payerKey, + tempAccount, + owner, + NATIVE_MINT + ) + ]; + + if (amountIn.gt(ZERO)) { + instructions.push(SystemProgram.transfer({ + fromPubkey: payerKey, + toPubkey: tempAccount, + lamports: amountIn.toNumber(), + })); + + instructions.push(createSyncNativeInstruction( + tempAccount, + )); + } + + const closeWSOLAccountInstruction = createCloseAccountInstruction( + tempAccount, + unwrapDestinationKey, + owner + ); + + return { + address: tempAccount, + instructions, + cleanupInstructions: [closeWSOLAccountInstruction], + signers: [], + }; +} + function createWrappedNativeAccountInstructionWithKeypair( owner: PublicKey, amountIn: BN, diff --git a/packages/common-sdk/tests/web3/ata-util.test.ts b/packages/common-sdk/tests/web3/ata-util.test.ts index b53063d..8031529 100644 --- a/packages/common-sdk/tests/web3/ata-util.test.ts +++ b/packages/common-sdk/tests/web3/ata-util.test.ts @@ -1,4 +1,5 @@ import { + ASSOCIATED_TOKEN_PROGRAM_ID, AccountLayout, NATIVE_MINT, TOKEN_PROGRAM_ID, @@ -300,7 +301,7 @@ describe("ata-util", () => { fail("should be failed"); } catch (e: any) { expect(e.name).toMatch("TokenOwnerOffCurveError"); - } + } }); it("resolveOrCreateATA, allowPDAOwnerAddress = true", async () => { @@ -322,7 +323,65 @@ describe("ata-util", () => { ); } catch (e: any) { fail("should be failed"); - } + } + }); + + it("resolveOrCreateATA, wrappedSolAccountCreateMethod = ata", async () => { + const { connection, wallet } = ctx; + + const wrappedSolAccountCreateMethod = "ata"; + + const resolved = await resolveOrCreateATA( + connection, + wallet.publicKey, + NATIVE_MINT, + () => connection.getMinimumBalanceForRentExemption(AccountLayout.span), + new BN(LAMPORTS_PER_SOL), + wallet.publicKey, + false, + false, + wrappedSolAccountCreateMethod, + ); + + expect(resolved.instructions.length).toEqual(3); + expect(resolved.instructions[0].programId.equals(ASSOCIATED_TOKEN_PROGRAM_ID)).toBeTruthy(); + expect(resolved.instructions[1].programId.equals(SystemProgram.programId)).toBeTruthy(); + expect(resolved.instructions[2].programId.equals(TOKEN_PROGRAM_ID)).toBeTruthy(); + expect(resolved.cleanupInstructions.length).toEqual(1); + expect(resolved.cleanupInstructions[0].programId.equals(TOKEN_PROGRAM_ID)).toBeTruthy(); + expect(resolved.signers.length).toEqual(0); + + const builder = new TransactionBuilder(connection, wallet); + builder.addInstruction(resolved); + await expect(builder.buildAndExecute()).resolves.toBeTruthy(); + }); + + it("resolveOrCreateATA, wrappedSolAccountCreateMethod = ata, amount = 0", async () => { + const { connection, wallet } = ctx; + + const wrappedSolAccountCreateMethod = "ata"; + + const resolved = await resolveOrCreateATA( + connection, + wallet.publicKey, + NATIVE_MINT, + () => connection.getMinimumBalanceForRentExemption(AccountLayout.span), + ZERO, + wallet.publicKey, + false, + false, + wrappedSolAccountCreateMethod, + ); + + expect(resolved.instructions.length).toEqual(1); + expect(resolved.instructions[0].programId.equals(ASSOCIATED_TOKEN_PROGRAM_ID)).toBeTruthy(); + expect(resolved.cleanupInstructions.length).toEqual(1); + expect(resolved.cleanupInstructions[0].programId.equals(TOKEN_PROGRAM_ID)).toBeTruthy(); + expect(resolved.signers.length).toEqual(0); + + const builder = new TransactionBuilder(connection, wallet); + builder.addInstruction(resolved); + await expect(builder.buildAndExecute()).resolves.toBeTruthy(); }); it("resolveOrCreateATA, wrappedSolAccountCreateMethod = keypair", async () => {