Skip to content

Commit

Permalink
(v0.3.2) Allow PDA as OwnerAddress / Add option to create WSOL accoun…
Browse files Browse the repository at this point in the history
…t with CreateAccountWithSeed (orca-so#53)

* fix testcase

* add allowPDAOwnerAddress, wsolAccountCreateMethod

* add rimraf

* bump to 0.3.2
  • Loading branch information
yugure-orca authored Jul 24, 2023
1 parent 5323d76 commit 6168141
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 45 deletions.
6 changes: 4 additions & 2 deletions packages/common-sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@orca-so/common-sdk",
"version": "0.3.1",
"version": "0.3.2",
"description": "Common Typescript components across Orca",
"repository": "https://github.com/orca-so/orca-sdks",
"author": "Orca Foundation",
Expand All @@ -23,11 +23,13 @@
"jest": "^29.5.0",
"prettier": "^2.3.2",
"process": "^0.11.10",
"rimraf": "^4.1.2",
"ts-jest": "^29.1.0",
"typescript": "^4.5.5"
},
"scripts": {
"build": "tsc -p src",
"build": "rimraf dist && tsc -p src",
"clean": "rimraf dist",
"watch": "tsc -w -p src",
"prepublishOnly": "yarn build",
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write",
Expand Down
25 changes: 19 additions & 6 deletions packages/common-sdk/src/web3/ata-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Connection, PublicKey } from "@solana/web3.js";
import BN from "bn.js";
import { ZERO } from "../math";
import { ParsableTokenAccountInfo, getMultipleParsedAccounts } from "./network";
import { ResolvedTokenAddressInstruction, TokenUtil } from "./token-util";
import { ResolvedTokenAddressInstruction, TokenUtil, WrappedSolAccountCreateMethod } from "./token-util";
import { EMPTY_INSTRUCTION } from "./transactions/types";

/**
Expand All @@ -22,6 +22,8 @@ import { EMPTY_INSTRUCTION } from "./transactions/types";
* @param wrappedSolAmountIn Optional. Only use for input/source token that could be SOL
* @param payer Payer that would pay the rent for the creation of the ATAs
* @param modeIdempotent Optional. Use CreateIdempotent instruction instead of Create instruction
* @param allowPDAOwnerAddress Optional. Allow PDA to be used as the ATA owner address
* @param wrappedSolAccountCreateMethod - Optional. How to create the temporary WSOL account.
* @returns
*/
export async function resolveOrCreateATA(
Expand All @@ -31,15 +33,19 @@ export async function resolveOrCreateATA(
getAccountRentExempt: () => Promise<number>,
wrappedSolAmountIn = ZERO,
payer = ownerAddress,
modeIdempotent: boolean = false
modeIdempotent: boolean = false,
allowPDAOwnerAddress: boolean = false,
wrappedSolAccountCreateMethod: WrappedSolAccountCreateMethod = "keypair",
): Promise<ResolvedTokenAddressInstruction> {
const instructions = await resolveOrCreateATAs(
connection,
ownerAddress,
[{ tokenMint, wrappedSolAmountIn }],
getAccountRentExempt,
payer,
modeIdempotent
modeIdempotent,
allowPDAOwnerAddress,
wrappedSolAccountCreateMethod,
);
return instructions[0]!;
}
Expand All @@ -60,6 +66,8 @@ type ResolvedTokenAddressRequest = {
* @param wrappedSolAmountIn Optional. Only use for input/source token that could be SOL
* @param payer Payer that would pay the rent for the creation of the ATAs
* @param modeIdempotent Optional. Use CreateIdempotent instruction instead of Create instruction
* @param allowPDAOwnerAddress Optional. Allow PDA to be used as the ATA owner address
* @param wrappedSolAccountCreateMethod - Optional. How to create the temporary WSOL account.
* @returns
*/
export async function resolveOrCreateATAs(
Expand All @@ -68,7 +76,9 @@ export async function resolveOrCreateATAs(
requests: ResolvedTokenAddressRequest[],
getAccountRentExempt: () => Promise<number>,
payer = ownerAddress,
modeIdempotent: boolean = false
modeIdempotent: boolean = false,
allowPDAOwnerAddress: boolean = false,
wrappedSolAccountCreateMethod: WrappedSolAccountCreateMethod = "keypair",
): Promise<ResolvedTokenAddressInstruction[]> {
const nonNativeMints = requests.filter(({ tokenMint }) => !tokenMint.equals(NATIVE_MINT));
const nativeMints = requests.filter(({ tokenMint }) => tokenMint.equals(NATIVE_MINT));
Expand All @@ -80,7 +90,7 @@ export async function resolveOrCreateATAs(
let instructionMap: { [tokenMint: string]: ResolvedTokenAddressInstruction } = {};
if (nonNativeMints.length > 0) {
const nonNativeAddresses = nonNativeMints.map(({ tokenMint }) =>
getAssociatedTokenAddressSync(tokenMint, ownerAddress)
getAssociatedTokenAddressSync(tokenMint, ownerAddress, allowPDAOwnerAddress)
);

const tokenAccounts = await getMultipleParsedAccounts(
Expand Down Expand Up @@ -132,7 +142,10 @@ export async function resolveOrCreateATAs(
instructionMap[NATIVE_MINT.toBase58()] = TokenUtil.createWrappedNativeAccountInstruction(
ownerAddress,
wrappedSolAmountIn,
accountRentExempt
accountRentExempt,
undefined, // use default
undefined, // use default
wrappedSolAccountCreateMethod
);
}

Expand Down
150 changes: 114 additions & 36 deletions packages/common-sdk/src/web3/token-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
getAssociatedTokenAddressSync,
} from "@solana/spl-token";
import { Connection, Keypair, PublicKey, SystemProgram } from "@solana/web3.js";
import { sha256 } from '@noble/hashes/sha256';
import BN from "bn.js";
import invariant from "tiny-invariant";
import { ZERO } from "../math";
Expand All @@ -19,6 +20,11 @@ export type ResolvedTokenAddressInstruction = {
address: PublicKey;
} & Instruction;

/**
* @category Util
*/
export type WrappedSolAccountCreateMethod = "keypair" | "withSeed";

/**
* @category Util
*/
Expand All @@ -29,50 +35,28 @@ export class TokenUtil {

/**
* Create an ix to send a native-mint and unwrap it to the user's wallet.
* @param owner
* @param amountIn
* @param rentExemptLamports
* @param payer
* @param unwrapDestination
* @param owner - PublicKey for the owner of the temporary WSOL account.
* @param amountIn - Amount of SOL to wrap.
* @param rentExemptLamports - Rent exempt lamports for the temporary WSOL account.
* @param payer - PublicKey for the payer that would fund the temporary WSOL accounts. (must sign the txn)
* @param unwrapDestination - PublicKey for the receiver that would receive the unwrapped SOL including rent.
* @param createAccountMethod - How to create the temporary WSOL account.
* @returns
*/
static createWrappedNativeAccountInstruction(
public static createWrappedNativeAccountInstruction(
owner: PublicKey,
amountIn: BN,
rentExemptLamports: number,
payer?: PublicKey,
unwrapDestination?: PublicKey
unwrapDestination?: PublicKey,
createAccountMethod: WrappedSolAccountCreateMethod = "keypair",
): ResolvedTokenAddressInstruction {
const payerKey = payer ?? owner;
const tempAccount = new Keypair();
const unwrapDestinationKey = unwrapDestination ?? payer ?? owner;

const createAccountInstruction = SystemProgram.createAccount({
fromPubkey: payerKey,
newAccountPubkey: tempAccount.publicKey,
lamports: amountIn.toNumber() + rentExemptLamports,
space: AccountLayout.span,
programId: TOKEN_PROGRAM_ID,
});

const initAccountInstruction = createInitializeAccountInstruction(
tempAccount.publicKey,
NATIVE_MINT,
owner
);

const closeWSOLAccountInstruction = createCloseAccountInstruction(
tempAccount.publicKey,
unwrapDestinationKey,
owner
);

return {
address: tempAccount.publicKey,
instructions: [createAccountInstruction, initAccountInstruction],
cleanupInstructions: [closeWSOLAccountInstruction],
signers: [tempAccount],
};
return createAccountMethod === "keypair"
? createWrappedNativeAccountInstructionWithKeypair(owner, amountIn, rentExemptLamports, payerKey, unwrapDestinationKey)
: createWrappedNativeAccountInstructionWithSeed(owner, amountIn, rentExemptLamports, payerKey, unwrapDestinationKey);
}

/**
Expand All @@ -88,6 +72,7 @@ export class TokenUtil {
* @param amount - Amount of token to send
* @param getAccountRentExempt - Fn to fetch the account rent exempt value
* @param payer - PublicKey for the payer that would fund the possibly new token-accounts. (must sign the txn)
* @param allowPDASourceWallet - Allow PDA to be used as the source wallet.
* @returns
*/
static async createSendTokensToWalletInstruction(
Expand All @@ -98,7 +83,8 @@ export class TokenUtil {
tokenDecimals: number,
amount: BN,
getAccountRentExempt: () => Promise<number>,
payer?: PublicKey
payer?: PublicKey,
allowPDASourceWallet: boolean = false
): Promise<Instruction> {
invariant(!amount.eq(ZERO), "SendToken transaction must send more than 0 tokens.");

Expand All @@ -116,7 +102,7 @@ export class TokenUtil {
};
}

const sourceTokenAccount = getAssociatedTokenAddressSync(tokenMint, sourceWallet);
const sourceTokenAccount = getAssociatedTokenAddressSync(tokenMint, sourceWallet, allowPDASourceWallet);
const { address: destinationTokenAccount, ...destinationAtaIx } = await resolveOrCreateATA(
connection,
destinationWallet,
Expand All @@ -142,3 +128,95 @@ export class TokenUtil {
};
}
}

function createWrappedNativeAccountInstructionWithKeypair(
owner: PublicKey,
amountIn: BN,
rentExemptLamports: number,
payerKey: PublicKey,
unwrapDestinationKey: PublicKey,
): ResolvedTokenAddressInstruction {
const tempAccount = new Keypair();

const createAccountInstruction = SystemProgram.createAccount({
fromPubkey: payerKey,
newAccountPubkey: tempAccount.publicKey,
lamports: amountIn.toNumber() + rentExemptLamports,
space: AccountLayout.span,
programId: TOKEN_PROGRAM_ID,
});

const initAccountInstruction = createInitializeAccountInstruction(
tempAccount.publicKey,
NATIVE_MINT,
owner
);

const closeWSOLAccountInstruction = createCloseAccountInstruction(
tempAccount.publicKey,
unwrapDestinationKey,
owner
);

return {
address: tempAccount.publicKey,
instructions: [createAccountInstruction, initAccountInstruction],
cleanupInstructions: [closeWSOLAccountInstruction],
signers: [tempAccount],
};
}

function createWrappedNativeAccountInstructionWithSeed(
owner: PublicKey,
amountIn: BN,
rentExemptLamports: number,
payerKey: PublicKey,
unwrapDestinationKey: PublicKey,
): ResolvedTokenAddressInstruction {
// seed is always shorter than a signature.
// So createWrappedNativeAccountInstructionWithSeed always generates small size instructions
// than createWrappedNativeAccountInstructionWithKeypair.
const seed = Keypair.generate().publicKey.toBase58().slice(0, 32); // 32 chars

const tempAccount = (() => {
// same to PublicKey.createWithSeed, but this one is synchronous
const fromPublicKey = owner;
const programId = TOKEN_PROGRAM_ID;
const buffer = Buffer.concat([
fromPublicKey.toBuffer(),
Buffer.from(seed),
programId.toBuffer(),
]);
const publicKeyBytes = sha256(buffer);
return new PublicKey(publicKeyBytes);
})();

const createAccountInstruction = SystemProgram.createAccountWithSeed({
fromPubkey: payerKey,
basePubkey: owner,
seed,
newAccountPubkey: tempAccount,
lamports: amountIn.toNumber() + rentExemptLamports,
space: AccountLayout.span,
programId: TOKEN_PROGRAM_ID,
});

const initAccountInstruction = createInitializeAccountInstruction(
tempAccount,
NATIVE_MINT,
owner
);

const closeWSOLAccountInstruction = createCloseAccountInstruction(
tempAccount,
unwrapDestinationKey,
owner
);

return {
address: tempAccount,
instructions: [createAccountInstruction, initAccountInstruction],
cleanupInstructions: [closeWSOLAccountInstruction],
signers: [],
};
}
Loading

0 comments on commit 6168141

Please sign in to comment.