/
getOrCreateAssociatedTokenAccount.ts
89 lines (83 loc) · 3.88 KB
/
getOrCreateAssociatedTokenAccount.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import type { Commitment, ConfirmOptions, Connection, PublicKey, Signer } from '@solana/web3.js';
import { sendAndConfirmTransaction, Transaction } from '@solana/web3.js';
import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../constants.js';
import {
TokenAccountNotFoundError,
TokenInvalidAccountOwnerError,
TokenInvalidMintError,
TokenInvalidOwnerError,
} from '../errors.js';
import { createAssociatedTokenAccountInstruction } from '../instructions/associatedTokenAccount.js';
import type { Account } from '../state/account.js';
import { getAccount } from '../state/account.js';
import { getAssociatedTokenAddressSync } from '../state/mint.js';
/**
* Retrieve the associated token account, or create it if it doesn't exist
*
* @param connection Connection to use
* @param payer Payer of the transaction and initialization fees
* @param mint Mint associated with the account to set or verify
* @param owner Owner of the account to set or verify
* @param allowOwnerOffCurve Allow the owner account to be a PDA (Program Derived Address)
* @param commitment Desired level of commitment for querying the state
* @param confirmOptions Options for confirming the transaction
* @param programId SPL Token program account
* @param associatedTokenProgramId SPL Associated Token program account
*
* @return Address of the new associated token account
*/
export async function getOrCreateAssociatedTokenAccount(
connection: Connection,
payer: Signer,
mint: PublicKey,
owner: PublicKey,
allowOwnerOffCurve = false,
commitment?: Commitment,
confirmOptions?: ConfirmOptions,
programId = TOKEN_PROGRAM_ID,
associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID
): Promise<Account> {
const associatedToken = getAssociatedTokenAddressSync(
mint,
owner,
allowOwnerOffCurve,
programId,
associatedTokenProgramId
);
// This is the optimal logic, considering TX fee, client-side computation, RPC roundtrips and guaranteed idempotent.
// Sadly we can't do this atomically.
let account: Account;
try {
account = await getAccount(connection, associatedToken, commitment, programId);
} catch (error: unknown) {
// TokenAccountNotFoundError can be possible if the associated address has already received some lamports,
// becoming a system account. Assuming program derived addressing is safe, this is the only case for the
// TokenInvalidAccountOwnerError in this code path.
if (error instanceof TokenAccountNotFoundError || error instanceof TokenInvalidAccountOwnerError) {
// As this isn't atomic, it's possible others can create associated accounts meanwhile.
try {
const transaction = new Transaction().add(
createAssociatedTokenAccountInstruction(
payer.publicKey,
associatedToken,
owner,
mint,
programId,
associatedTokenProgramId
)
);
await sendAndConfirmTransaction(connection, transaction, [payer], confirmOptions);
} catch (error: unknown) {
// Ignore all errors; for now there is no API-compatible way to selectively ignore the expected
// instruction error if the associated account exists already.
}
// Now this should always succeed
account = await getAccount(connection, associatedToken, commitment, programId);
} else {
throw error;
}
}
if (!account.mint.equals(mint)) throw new TokenInvalidMintError();
if (!account.owner.equals(owner)) throw new TokenInvalidOwnerError();
return account;
}