@xgamesfun/solana 0.0.0-stage → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLAIMER.md +41 -0
- package/LICENSE +202 -0
- package/NOTICE +13 -0
- package/README.md +172 -2
- package/bin/xsolana.mjs +189 -0
- package/examples/artwork-auction.json +51 -0
- package/examples/blind-forge.json +128 -0
- package/examples/coin-flip-arena.json +103 -0
- package/examples/duel-arena.json +137 -0
- package/examples/elements-set.json +274 -0
- package/examples/event-badges.json +67 -0
- package/examples/expedition-guild.json +154 -0
- package/examples/fair-presale.json +48 -0
- package/examples/falling-drop.json +90 -0
- package/examples/guild-missions.json +137 -0
- package/examples/halving-seasons.json +107 -0
- package/examples/hero-trials.json +93 -0
- package/examples/ideas.json +789 -0
- package/examples/level-ladder.json +65 -0
- package/examples/merge-2048.json +128 -0
- package/examples/nft-raffle.json +33 -0
- package/examples/paper-hands-farm.json +44 -0
- package/examples/partner-vault.json +112 -0
- package/examples/sacrifice-raffle.json +118 -0
- package/examples/scorched-treasury.json +84 -0
- package/examples/shelter-fair.json +164 -0
- package/examples/spark-pity.json +153 -0
- package/examples/stakers-draw.json +118 -0
- package/examples/star-fleet.json +150 -0
- package/examples/starforce.json +116 -0
- package/examples/stud-farm.json +134 -0
- package/examples/team-vesting.json +70 -0
- package/examples/trade-in-shop.json +168 -0
- package/js/approve/details.mjs +263 -0
- package/js/approve/ed25519.mjs +36 -0
- package/js/approve/live-check.mjs +144 -0
- package/js/approve/page.html +86 -0
- package/js/approve/page.js +203 -0
- package/js/approve/server.mjs +726 -0
- package/js/chain/base58.js +73 -0
- package/js/chain/borsh.js +270 -0
- package/js/chain/bytes.js +141 -0
- package/js/chain/core.js +181 -0
- package/js/chain/curve.js +52 -0
- package/js/chain/lookup.js +279 -0
- package/js/chain/mint.js +96 -0
- package/js/chain/pda.js +66 -0
- package/js/chain/programs.js +102 -0
- package/js/chain/rpc.js +219 -0
- package/js/chain/sha256.js +68 -0
- package/js/chain/token.js +85 -0
- package/js/chain/token2022.js +247 -0
- package/js/chain/tx.js +224 -0
- package/js/client/accounts.js +679 -0
- package/js/client/compressed.js +46 -0
- package/js/client/grant.js +299 -0
- package/js/client/idl/loot.js +2 -0
- package/js/client/idl/loot.json +17206 -0
- package/js/client/index.js +10 -0
- package/js/client/ix.js +2887 -0
- package/js/client/pda.js +114 -0
- package/js/client/policy.js +69 -0
- package/js/client/random.js +70 -0
- package/js/client/sizes.js +297 -0
- package/js/client/traits.js +44 -0
- package/js/connect/connect.mjs +182 -0
- package/js/economy/check.mjs +2236 -0
- package/js/economy/cost.mjs +185 -0
- package/js/economy/examples.mjs +114 -0
- package/js/economy/explain.mjs +239 -0
- package/js/economy/guide.mjs +232 -0
- package/js/economy/legal.mjs +15 -0
- package/js/economy/live.mjs +130 -0
- package/js/economy/model.mjs +698 -0
- package/js/economy/plan.mjs +1155 -0
- package/js/economy/schema.json +3071 -0
- package/js/economy/secrets.mjs +60 -0
- package/js/economy/simulate.mjs +1019 -0
- package/js/economy/txsize.mjs +45 -0
- package/js/economy/validate.mjs +85 -0
- package/js/index.js +10 -0
- package/js/mcp/server.mjs +113 -0
- package/js/mcp/tools.mjs +485 -0
- package/js/play/index.js +2855 -0
- package/js/play/keys.js +167 -0
- package/js/results/index.js +184 -0
- package/js/results/wire.js +90 -0
- package/package.json +100 -4
- package/release.json +8 -0
- package/skills/xsolana-economy/SKILL.md +109 -0
- package/skills/xsolana-economy/references/after-launch.md +124 -0
- package/skills/xsolana-economy/references/apps.md +193 -0
- package/skills/xsolana-economy/references/sentence-to-spec.md +180 -0
- package/skills/xsolana-economy/references/telling-the-person.md +134 -0
- package/types/chain/base58.d.ts +10 -0
- package/types/chain/borsh.d.ts +23 -0
- package/types/chain/bytes.d.ts +45 -0
- package/types/chain/core.d.ts +52 -0
- package/types/chain/curve.d.ts +1 -0
- package/types/chain/lookup.d.ts +194 -0
- package/types/chain/mint.d.ts +25 -0
- package/types/chain/pda.d.ts +25 -0
- package/types/chain/programs.d.ts +65 -0
- package/types/chain/rpc.d.ts +68 -0
- package/types/chain/sha256.d.ts +1 -0
- package/types/chain/token.d.ts +141 -0
- package/types/chain/token2022.d.ts +53 -0
- package/types/chain/tx.d.ts +60 -0
- package/types/client/accounts.d.ts +318 -0
- package/types/client/compressed.d.ts +29 -0
- package/types/client/grant.d.ts +170 -0
- package/types/client/idl/loot.d.ts +4498 -0
- package/types/client/index.d.ts +9 -0
- package/types/client/ix.d.ts +5315 -0
- package/types/client/pda.d.ts +95 -0
- package/types/client/policy.d.ts +48 -0
- package/types/client/random.d.ts +26 -0
- package/types/client/sizes.d.ts +101 -0
- package/types/client/traits.d.ts +12 -0
- package/types/economy/check.d.mts +100 -0
- package/types/economy/cost.d.mts +69 -0
- package/types/economy/examples.d.mts +27 -0
- package/types/economy/explain.d.mts +19 -0
- package/types/economy/guide.d.mts +140 -0
- package/types/economy/legal.d.mts +12 -0
- package/types/economy/live.d.mts +124 -0
- package/types/economy/model.d.mts +431 -0
- package/types/economy/plan.d.mts +1014 -0
- package/types/economy/secrets.d.mts +4 -0
- package/types/economy/simulate.d.mts +164 -0
- package/types/economy/txsize.d.mts +17 -0
- package/types/economy/validate.d.mts +3 -0
- package/types/index.d.ts +9 -0
- package/types/mcp/tools.d.mts +635 -0
- package/types/play/index.d.ts +880 -0
- package/types/play/keys.d.ts +51 -0
- package/types/results/index.d.ts +155 -0
- package/types/results/wire.d.ts +122 -0
package/js/chain/rpc.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// The Solana JSON-RPC calls this kit makes, over fetch, for browser apps and Node alike. Public RPCs rate-limit
|
|
2
|
+
// (devnet: about 100 requests per 10 s per IP, 40 for any one method), so 429s are retried with backoff, and so are a
|
|
3
|
+
// gateway's passing failures (502, 503, 504).
|
|
4
|
+
import { fromBase64, toBase64 } from './bytes.js';
|
|
5
|
+
|
|
6
|
+
// The options of a call made without any: read-only, and typed so the generated declarations take any option.
|
|
7
|
+
const NO_OPTIONS = Object.freeze(/** @type {Record<string, any>} */ ({}));
|
|
8
|
+
|
|
9
|
+
const TIMEOUT_MS = 20_000;
|
|
10
|
+
const RETRIES = 4;
|
|
11
|
+
// A rate limit is waited out longer than a failed connection: about a minute in all, with jitter so pages and
|
|
12
|
+
// servers sharing an IP don't retry in step.
|
|
13
|
+
const RATE_LIMIT_RETRIES = 7;
|
|
14
|
+
const CONFIRM_POLL_MS = 1200;
|
|
15
|
+
const CONFIRM_TIMEOUT_MS = 90_000;
|
|
16
|
+
const ACCOUNTS_PER_CALL = 100;
|
|
17
|
+
/** The HTTP statuses of a gateway that failed for a moment (bad gateway, unavailable, timed out): retried. */
|
|
18
|
+
const PASSING = [502, 503, 504];
|
|
19
|
+
/** The JSON-RPC errors of a node behind the cluster: retried. */
|
|
20
|
+
const BEHIND = [-32005, -32016];
|
|
21
|
+
|
|
22
|
+
/** A JSON-RPC failure, with the node's own message (and logs for a failed simulation). */
|
|
23
|
+
export class RpcError extends Error {
|
|
24
|
+
constructor(message, { code, logs } = NO_OPTIONS) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = 'RpcError';
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.logs = logs;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
33
|
+
|
|
34
|
+
/** The first block of each public Solana cluster: it names the cluster whatever URL reaches it. */
|
|
35
|
+
export const GENESIS = Object.freeze({
|
|
36
|
+
devnet: 'EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG',
|
|
37
|
+
mainnet: '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d',
|
|
38
|
+
testnet: '4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const DEVNET_ONLY = 'loot runs on devnet only: mainnet waits for an audit. Unset XSOLANA_RPC_URL, or point it at a devnet RPC.';
|
|
42
|
+
|
|
43
|
+
/** Refuses an RPC URL that names mainnet. Returns the URL. */
|
|
44
|
+
export function notMainnet(url) {
|
|
45
|
+
if (/mainnet/i.test(String(url))) throw new RpcError(`${url} is a mainnet RPC. ${DEVNET_ONLY}`);
|
|
46
|
+
return url;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Refuses a chain that is mainnet or testnet, by its genesis block, whatever its URL says (a local validator is
|
|
51
|
+
* let through). `rpc` has `call`. Returns the genesis hash.
|
|
52
|
+
*/
|
|
53
|
+
export async function checkCluster(rpc) {
|
|
54
|
+
const genesis = await rpc.call('getGenesisHash');
|
|
55
|
+
if (genesis === GENESIS.mainnet) throw new RpcError(`${rpc.url ?? 'This RPC'} is on mainnet. ${DEVNET_ONLY}`);
|
|
56
|
+
if (genesis === GENESIS.testnet) throw new RpcError(`${rpc.url ?? 'This RPC'} is on testnet. ${DEVNET_ONLY}`);
|
|
57
|
+
return genesis;
|
|
58
|
+
}
|
|
59
|
+
const decodeAccount = (raw) =>
|
|
60
|
+
raw ? { owner: raw.owner, lamports: raw.lamports, data: fromBase64(raw.data[0]), executable: raw.executable } : null;
|
|
61
|
+
|
|
62
|
+
/** The program's own words for a failed run (`AnchorError … Error Message: …`), else the last error log line. A program
|
|
63
|
+
* built without its errors' text logs the error's name as its message: then the name alone (the client's IDL has
|
|
64
|
+
* the sentence: `loot.lootError`). */
|
|
65
|
+
export function programError(err, logs) {
|
|
66
|
+
const anchor = logs?.find((line) => line.includes('AnchorError'));
|
|
67
|
+
const said = anchor ? /Error Message: (.+?)\.?$/.exec(anchor)?.[1] : undefined;
|
|
68
|
+
const code = anchor ? /Error Code: (\w+)/.exec(anchor)?.[1] : undefined;
|
|
69
|
+
const last = logs?.filter((l) => /error|failed|insufficient/i.test(l)).at(-1);
|
|
70
|
+
if (said) return code && code !== said ? `${said} (${code})` : said;
|
|
71
|
+
return last ?? (typeof err === 'string' ? err : JSON.stringify(err));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A client for one RPC URL. `fetchImpl` and `sleepImpl` are for tests. */
|
|
75
|
+
export function rpcClient(url, { fetchImpl = globalThis.fetch, sleepImpl = sleep } = {}) {
|
|
76
|
+
let id = 0;
|
|
77
|
+
async function call(method, params = []) {
|
|
78
|
+
for (let attempt = 0; ; attempt++) {
|
|
79
|
+
let response;
|
|
80
|
+
try {
|
|
81
|
+
response = await fetchImpl(url, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: { 'content-type': 'application/json' },
|
|
84
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: ++id, method, params }),
|
|
85
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
86
|
+
});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (attempt < RETRIES) {
|
|
89
|
+
await sleepImpl(500 * 2 ** attempt);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
throw new RpcError(`RPC ${method} did not answer: ${error.message}`);
|
|
93
|
+
}
|
|
94
|
+
if (response.status === 429 && attempt < RATE_LIMIT_RETRIES) {
|
|
95
|
+
const after = Number(response.headers?.get?.('retry-after'));
|
|
96
|
+
await sleepImpl(after > 0 ? after * 1000 : Math.min(1000 * 2 ** attempt, 15_000) * (0.75 + Math.random() / 2));
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// Sending the same signed transaction again is safe: it has one signature, so it lands once at most.
|
|
100
|
+
if (PASSING.includes(response.status) && attempt < RETRIES) {
|
|
101
|
+
await sleepImpl(500 * 2 ** attempt);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const body = await response.json().catch(() => null);
|
|
105
|
+
if (!response.ok || !body || body.error) {
|
|
106
|
+
const message = body?.error?.message ?? `RPC ${method} failed (HTTP ${response.status})`;
|
|
107
|
+
if (body?.error?.code === 429 && attempt < RETRIES) {
|
|
108
|
+
await sleepImpl(1000 * 2 ** attempt);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
// A node of a load-balanced RPC a moment behind: "No confirmed state available yet" (-32005), "Minimum context
|
|
112
|
+
// slot has not been reached" (-32016). Asked again, another (or the same, caught up) answers.
|
|
113
|
+
if (BEHIND.includes(body?.error?.code) && attempt < RETRIES) {
|
|
114
|
+
await sleepImpl(500 * 2 ** attempt);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// A send whose preflight failed ("Transaction simulation failed: … custom program error: 0x17b5") carries the
|
|
118
|
+
// logs, where the program says why in words.
|
|
119
|
+
const logs = body?.error?.data?.logs;
|
|
120
|
+
const said = logs?.length ? `The network refused this in a dry run: ${programError(body.error.data.err, logs)}` : message;
|
|
121
|
+
throw new RpcError(said, { code: body?.error?.code, logs });
|
|
122
|
+
}
|
|
123
|
+
return body.result;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function accounts(addresses, commitment = 'confirmed', { minContextSlot } = NO_OPTIONS) {
|
|
128
|
+
const out = [];
|
|
129
|
+
for (let i = 0; i < addresses.length; i += ACCOUNTS_PER_CALL) {
|
|
130
|
+
const batch = addresses.slice(i, i + ACCOUNTS_PER_CALL);
|
|
131
|
+
const r = await call('getMultipleAccounts', [batch, { encoding: 'base64', commitment, ...(minContextSlot != null ? { minContextSlot: Number(minContextSlot) } : {}) }]);
|
|
132
|
+
out.push(...r.value.map(decodeAccount));
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function signatureStatus(signature) {
|
|
138
|
+
const r = await call('getSignatureStatuses', [[signature], { searchTransactionHistory: false }]);
|
|
139
|
+
return r.value[0];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
url,
|
|
144
|
+
call,
|
|
145
|
+
/** Accounts as of `commitment`; `minContextSlot`: from a node that has seen at least that slot (one's own landed send). */
|
|
146
|
+
accounts,
|
|
147
|
+
async account(address, commitment = 'confirmed', options = {}) {
|
|
148
|
+
return (await accounts([address], commitment, options))[0];
|
|
149
|
+
},
|
|
150
|
+
async latestBlockhash() {
|
|
151
|
+
const r = await call('getLatestBlockhash', [{ commitment: 'confirmed' }]);
|
|
152
|
+
return { blockhash: r.value.blockhash, lastValidBlockHeight: r.value.lastValidBlockHeight };
|
|
153
|
+
},
|
|
154
|
+
async blockHeight() {
|
|
155
|
+
return call('getBlockHeight', [{ commitment: 'confirmed' }]);
|
|
156
|
+
},
|
|
157
|
+
async balance(address) {
|
|
158
|
+
return BigInt((await call('getBalance', [address, { commitment: 'confirmed' }])).value);
|
|
159
|
+
},
|
|
160
|
+
/**
|
|
161
|
+
* A token account's balance in base units, or 0n when there is no account there. Any other refusal throws: an
|
|
162
|
+
* address that is not a token account (a wallet's, passed by mistake: "Invalid param: not a Token account") is
|
|
163
|
+
* not an empty one.
|
|
164
|
+
*/
|
|
165
|
+
async tokenBalance(tokenAccount) {
|
|
166
|
+
try {
|
|
167
|
+
return BigInt((await call('getTokenAccountBalance', [tokenAccount, { commitment: 'confirmed' }])).value.amount);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (/could not find account|account not found/i.test(error.message)) return 0n;
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
async programAccounts(program, filters) {
|
|
174
|
+
const r = await call('getProgramAccounts', [program, { encoding: 'base64', commitment: 'confirmed', filters }]);
|
|
175
|
+
return r.map((x) => ({ pubkey: x.pubkey, account: decodeAccount(x.account) }));
|
|
176
|
+
},
|
|
177
|
+
/**
|
|
178
|
+
* Dry-run a wire transaction (empty signature slots are fine). Throws with the program's reason when it fails.
|
|
179
|
+
* `accounts`: addresses whose state after the run to return (base64-decoded, or null).
|
|
180
|
+
*/
|
|
181
|
+
async simulate(wire, { accounts: watch = [], replaceBlockhash = false } = {}) {
|
|
182
|
+
const r = await call('simulateTransaction', [
|
|
183
|
+
toBase64(wire),
|
|
184
|
+
{
|
|
185
|
+
encoding: 'base64',
|
|
186
|
+
sigVerify: false,
|
|
187
|
+
replaceRecentBlockhash: replaceBlockhash,
|
|
188
|
+
commitment: 'confirmed',
|
|
189
|
+
...(watch.length ? { accounts: { encoding: 'base64', addresses: watch } } : {}),
|
|
190
|
+
},
|
|
191
|
+
]);
|
|
192
|
+
const value = r.value;
|
|
193
|
+
if (value.err) throw new RpcError(`The network refused this in a dry run: ${programError(value.err, value.logs)}`, { logs: value.logs });
|
|
194
|
+
return { unitsConsumed: value.unitsConsumed ?? null, logs: value.logs ?? [], accounts: (value.accounts ?? []).map(decodeAccount) };
|
|
195
|
+
},
|
|
196
|
+
async send(wire) {
|
|
197
|
+
return call('sendTransaction', [toBase64(wire), { encoding: 'base64', preflightCommitment: 'confirmed', maxRetries: 3 }]);
|
|
198
|
+
},
|
|
199
|
+
signatureStatus,
|
|
200
|
+
/** Wait until a sent transaction is confirmed; throws with the program's reason when it failed. */
|
|
201
|
+
async confirm(signature, { timeoutMs = CONFIRM_TIMEOUT_MS, lastValidBlockHeight = null } = {}) {
|
|
202
|
+
const deadline = Date.now() + timeoutMs;
|
|
203
|
+
for (let poll = 0; Date.now() < deadline; poll++) {
|
|
204
|
+
const status = await signatureStatus(signature);
|
|
205
|
+
if (status?.err) {
|
|
206
|
+
const tx = await call('getTransaction', [signature, { commitment: 'confirmed', maxSupportedTransactionVersion: 0 }]).catch(() => null);
|
|
207
|
+
throw new RpcError(`Transaction ${signature} failed: ${programError(status.err, tx?.meta?.logMessages)}`, { logs: tx?.meta?.logMessages });
|
|
208
|
+
}
|
|
209
|
+
if (status?.confirmationStatus === 'confirmed' || status?.confirmationStatus === 'finalized') return signature;
|
|
210
|
+
if (lastValidBlockHeight !== null && poll % 5 === 4) {
|
|
211
|
+
const height = await call('getBlockHeight', [{ commitment: 'confirmed' }]).catch(() => 0);
|
|
212
|
+
if (height > lastValidBlockHeight) throw new RpcError(`Transaction ${signature} expired before it landed. Try again.`);
|
|
213
|
+
}
|
|
214
|
+
await sleepImpl(CONFIRM_POLL_MS);
|
|
215
|
+
}
|
|
216
|
+
throw new RpcError(`Transaction ${signature} was not confirmed in time; check it in the explorer.`);
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// SHA-256 (FIPS 180-4), synchronous. Program addresses are derived in browser apps and Node alike, and WebCrypto's
|
|
2
|
+
// digest is asynchronous and missing outside secure contexts, so this carries its own.
|
|
3
|
+
|
|
4
|
+
const K = Uint32Array.from([
|
|
5
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98,
|
|
6
|
+
0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
|
|
7
|
+
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8,
|
|
8
|
+
0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
|
9
|
+
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819,
|
|
10
|
+
0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
|
|
11
|
+
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
|
|
12
|
+
0xc67178f2,
|
|
13
|
+
]);
|
|
14
|
+
const INITIAL = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
|
|
15
|
+
const BLOCK_BYTES = 64;
|
|
16
|
+
|
|
17
|
+
const rotr = (x, n) => (x >>> n) | (x << (32 - n));
|
|
18
|
+
|
|
19
|
+
/** The message padded to whole 64-byte blocks, with its bit length at the end. */
|
|
20
|
+
function padded(message) {
|
|
21
|
+
const length = Math.ceil((message.length + 9) / BLOCK_BYTES) * BLOCK_BYTES;
|
|
22
|
+
const out = new Uint8Array(length);
|
|
23
|
+
out.set(message);
|
|
24
|
+
out[message.length] = 0x80;
|
|
25
|
+
const view = new DataView(out.buffer);
|
|
26
|
+
const bits = message.length * 8;
|
|
27
|
+
view.setUint32(length - 8, Math.floor(bits / 2 ** 32));
|
|
28
|
+
view.setUint32(length - 4, bits >>> 0);
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function compress(state, view, offset, w) {
|
|
33
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(offset + i * 4);
|
|
34
|
+
for (let i = 16; i < 64; i++) {
|
|
35
|
+
const a = w[i - 15];
|
|
36
|
+
const b = w[i - 2];
|
|
37
|
+
const s0 = rotr(a, 7) ^ rotr(a, 18) ^ (a >>> 3);
|
|
38
|
+
const s1 = rotr(b, 17) ^ rotr(b, 19) ^ (b >>> 10);
|
|
39
|
+
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
|
|
40
|
+
}
|
|
41
|
+
let [a, b, c, d, e, f, g, h] = state;
|
|
42
|
+
for (let i = 0; i < 64; i++) {
|
|
43
|
+
const t1 = (h + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + ((e & f) ^ (~e & g)) + K[i] + w[i]) >>> 0;
|
|
44
|
+
const t2 = ((rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0;
|
|
45
|
+
h = g;
|
|
46
|
+
g = f;
|
|
47
|
+
f = e;
|
|
48
|
+
e = (d + t1) >>> 0;
|
|
49
|
+
d = c;
|
|
50
|
+
c = b;
|
|
51
|
+
b = a;
|
|
52
|
+
a = (t1 + t2) >>> 0;
|
|
53
|
+
}
|
|
54
|
+
const next = [a, b, c, d, e, f, g, h];
|
|
55
|
+
for (let i = 0; i < 8; i++) state[i] = (state[i] + next[i]) >>> 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function sha256(message) {
|
|
59
|
+
const data = padded(message);
|
|
60
|
+
const view = new DataView(data.buffer);
|
|
61
|
+
const state = [...INITIAL];
|
|
62
|
+
const w = new Uint32Array(64);
|
|
63
|
+
for (let offset = 0; offset < data.length; offset += BLOCK_BYTES) compress(state, view, offset, w);
|
|
64
|
+
const out = new Uint8Array(32);
|
|
65
|
+
const outView = new DataView(out.buffer);
|
|
66
|
+
state.forEach((word, i) => outView.setUint32(i * 4, word));
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The System and SPL Token instructions a project's setup needs (a new mint, its first tokens, handing its authority to
|
|
2
|
+
// the project), as plain data. Classic Token and Token-2022 share these layouts.
|
|
3
|
+
import { addressBytes } from './base58.js';
|
|
4
|
+
import { concat, uintLE, utf8 } from './bytes.js';
|
|
5
|
+
import { SYSTEM_PROGRAM, TOKEN_PROGRAM } from './pda.js';
|
|
6
|
+
import { meta } from './programs.js';
|
|
7
|
+
|
|
8
|
+
/** A classic mint's size. */
|
|
9
|
+
export const MINT_BYTES = 82;
|
|
10
|
+
/** A classic token account's size. */
|
|
11
|
+
export const TOKEN_ACCOUNT_BYTES = 165;
|
|
12
|
+
|
|
13
|
+
/** System `CreateAccount`: `from` pays `lamports` for `space` bytes at `address` (which signs), owned by `owner`. */
|
|
14
|
+
export function createAccountIx({ from, address, lamports, space, owner }) {
|
|
15
|
+
return {
|
|
16
|
+
programId: SYSTEM_PROGRAM,
|
|
17
|
+
keys: [meta(from, true, true), meta(address, true, true)],
|
|
18
|
+
data: concat(uintLE(0, 4), uintLE(lamports, 8), uintLE(space, 8), addressBytes(owner)),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* System `CreateAccountWithSeed`: `from` pays `lamports` for `space` bytes at `addressWithSeed(from, seed, owner)`, owned
|
|
24
|
+
* by `owner`. `from` is the base too, so it alone signs.
|
|
25
|
+
*/
|
|
26
|
+
export function createAccountWithSeedIx({ from, address, seed, lamports, space, owner }) {
|
|
27
|
+
const text = utf8(seed);
|
|
28
|
+
return {
|
|
29
|
+
programId: SYSTEM_PROGRAM,
|
|
30
|
+
keys: [meta(from, true, true), meta(address, false, true)],
|
|
31
|
+
data: concat(uintLE(3, 4), addressBytes(from), uintLE(text.length, 8), text, uintLE(lamports, 8), uintLE(space, 8), addressBytes(owner)),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* System `AllocateWithSeed`: the account at `addressWithSeed(base, seed, owner)`, which holds only lamports (System's, no
|
|
37
|
+
* data), gets `space` bytes and becomes `owner`'s. `base` alone signs. Unlike `CreateAccountWithSeed` it takes an address
|
|
38
|
+
* someone already sent lamports to; whoever pays tops it up to the rent first (a System transfer).
|
|
39
|
+
*/
|
|
40
|
+
export function allocateWithSeedIx({ base, address, seed, space, owner }) {
|
|
41
|
+
const text = utf8(seed);
|
|
42
|
+
return {
|
|
43
|
+
programId: SYSTEM_PROGRAM,
|
|
44
|
+
keys: [meta(address, false, true), meta(base, true, false)],
|
|
45
|
+
data: concat(uintLE(9, 4), addressBytes(base), uintLE(text.length, 8), text, uintLE(space, 8), addressBytes(owner)),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Token `InitializeAccount3`: `account` (made empty first) becomes a token account of `mint` that `owner` holds. */
|
|
50
|
+
export const initializeAccount3Ix = ({ account, mint, owner, tokenProgram = TOKEN_PROGRAM }) => ({
|
|
51
|
+
programId: tokenProgram,
|
|
52
|
+
keys: [meta(account, false, true), meta(mint, false, false)],
|
|
53
|
+
data: concat(Uint8Array.of(18), addressBytes(owner)),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/** Token `InitializeMint2`. */
|
|
57
|
+
export function initializeMint2Ix({ mint, decimals, authority, freezeAuthority = null, tokenProgram = TOKEN_PROGRAM }) {
|
|
58
|
+
const freeze = freezeAuthority ? concat(Uint8Array.of(1), addressBytes(freezeAuthority)) : Uint8Array.of(0);
|
|
59
|
+
return {
|
|
60
|
+
programId: tokenProgram,
|
|
61
|
+
keys: [meta(mint, false, true)],
|
|
62
|
+
data: concat(Uint8Array.of(20, decimals), addressBytes(authority), freeze),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Token `MintTo`: `authority` (the mint authority, which signs) mints `amount` into the token account `to`. */
|
|
67
|
+
export const mintToIx = ({ mint, to, authority, amount, tokenProgram = TOKEN_PROGRAM }) => ({
|
|
68
|
+
programId: tokenProgram,
|
|
69
|
+
keys: [meta(mint, false, true), meta(to, false, true), meta(authority, true, false)],
|
|
70
|
+
data: concat(Uint8Array.of(7), uintLE(amount, 8)),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/** Token `TransferChecked`. */
|
|
74
|
+
export const transferCheckedIx = ({ from, mint, to, owner, amount, decimals, tokenProgram = TOKEN_PROGRAM }) => ({
|
|
75
|
+
programId: tokenProgram,
|
|
76
|
+
keys: [meta(from, false, true), meta(mint, false, false), meta(to, false, true), meta(owner, true, false)],
|
|
77
|
+
data: concat(Uint8Array.of(12), uintLE(amount, 8), Uint8Array.of(decimals)),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/** Token `SetAuthority` of a mint's minting (`kind` 0) or freezing (1) to `newAuthority` (null: none). */
|
|
81
|
+
export const setAuthorityIx = ({ account, current, kind = 0, newAuthority, tokenProgram = TOKEN_PROGRAM }) => ({
|
|
82
|
+
programId: tokenProgram,
|
|
83
|
+
keys: [meta(account, false, true), meta(current, true, false)],
|
|
84
|
+
data: concat(Uint8Array.of(6, kind), newAuthority ? concat(Uint8Array.of(1), addressBytes(newAuthority)) : Uint8Array.of(0)),
|
|
85
|
+
});
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// What a Token-2022 transfer needs beyond the transfer itself, read from the mint. The loot program moves every token
|
|
2
|
+
// with TransferChecked and hands each Token-2022 transfer the instruction's remaining accounts,
|
|
3
|
+
// so the caller passes:
|
|
4
|
+
// - the Memo program, which the program calls first when the receiving account requires a memo. It is always passed
|
|
5
|
+
// for a Token-2022 mint: any receiver can turn memos on between the moment a transaction is built and the moment
|
|
6
|
+
// it lands, and one account more costs nothing;
|
|
7
|
+
// - for a mint with a transfer hook: the hook program, its list of extra accounts ("extra-account-metas", mint) and
|
|
8
|
+
// every account that list names, resolved here as spl-transfer-hook-interface does. Token-2022 takes the ones its
|
|
9
|
+
// hook names by address and ignores the rest. The hook never gets a signature: every account is passed unsigned.
|
|
10
|
+
// Classic SPL mints need none of it.
|
|
11
|
+
import { addressBytes, encodeBase58, NO_ADDRESS } from './base58.js';
|
|
12
|
+
import { concat, readUintLE, uintLE, utf8 } from './bytes.js';
|
|
13
|
+
import { pdaOf, TOKEN_PROGRAM, TOKEN_2022_PROGRAM } from './pda.js';
|
|
14
|
+
import { meta } from './programs.js';
|
|
15
|
+
import { MINT_BYTES, TOKEN_ACCOUNT_BYTES } from './token.js';
|
|
16
|
+
|
|
17
|
+
export const MEMO_PROGRAM = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr';
|
|
18
|
+
/** The hook interface's Execute instruction: its discriminator, then the amount (u64). */
|
|
19
|
+
export const EXECUTE_DISCRIMINATOR = Uint8Array.of(105, 37, 101, 197, 75, 251, 102, 26);
|
|
20
|
+
|
|
21
|
+
const ACCOUNT_TYPE_MINT = 1;
|
|
22
|
+
const EXTENSION = { DefaultAccountState: 6, MemoTransfer: 8, PermanentDelegate: 12, TransferHook: 14, PausableConfig: 26 };
|
|
23
|
+
const META_BYTES = 35;
|
|
24
|
+
|
|
25
|
+
function checkedSlice(data, at, length, what) {
|
|
26
|
+
if (!Number.isInteger(at) || !Number.isInteger(length) || at < 0 || length < 0 || at + length > data.length) throw new RangeError(`${what} runs past the data`);
|
|
27
|
+
return data.subarray(at, at + length);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The value of extension `type` in a Token-2022 mint or account, or null. */
|
|
31
|
+
export function extension(data, type) {
|
|
32
|
+
let at = TOKEN_ACCOUNT_BYTES + 1;
|
|
33
|
+
while (at + 4 <= data.length) {
|
|
34
|
+
const kind = Number(readUintLE(data, at, 2));
|
|
35
|
+
const length = Number(readUintLE(data, at + 2, 2));
|
|
36
|
+
if (at + 4 + length > data.length) throw new RangeError('extension runs past the data');
|
|
37
|
+
if (kind === type) return checkedSlice(data, at + 4, length, 'extension');
|
|
38
|
+
if (kind === 0) return null;
|
|
39
|
+
at += 4 + length;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The transfer hook program a Token-2022 mint names, or null. */
|
|
45
|
+
export function hookProgramOf(mintData) {
|
|
46
|
+
if (mintData.length <= TOKEN_ACCOUNT_BYTES || mintData[TOKEN_ACCOUNT_BYTES] !== ACCOUNT_TYPE_MINT) return null;
|
|
47
|
+
const value = extension(mintData, EXTENSION.TransferHook);
|
|
48
|
+
if (!value || value.length < 64) return null;
|
|
49
|
+
const program = encodeBase58(value.subarray(32, 64));
|
|
50
|
+
return program === NO_ADDRESS ? null : program;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether a mint's new accounts start frozen, or can be made to (as the program judges it): a Token-2022
|
|
55
|
+
* mint carrying `DefaultAccountState` set to frozen, or set to anything while it has a freeze authority, which can switch
|
|
56
|
+
* it to frozen whenever it likes. An auction, a contest, a lottery, an offer and a sealed schedule refuse such a mint:
|
|
57
|
+
* each reads the setting once, as it opens, and a refund or payout opened into a frozen account would never land.
|
|
58
|
+
*/
|
|
59
|
+
export function mayStartFrozen({ owner, data }) {
|
|
60
|
+
if (owner !== TOKEN_2022_PROGRAM || data.length <= TOKEN_ACCOUNT_BYTES || data[TOKEN_ACCOUNT_BYTES] !== ACCOUNT_TYPE_MINT) return false;
|
|
61
|
+
const state = extension(data, EXTENSION.DefaultAccountState);
|
|
62
|
+
if (!state) return false;
|
|
63
|
+
const freezer = data.length >= 50 && readUintLE(data, 46, 4) !== 0n;
|
|
64
|
+
return state[0] === 2 || freezer;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Who can stop a mint's tokens moving, read from its account (`owner`, its token program; `data`): its freeze authority
|
|
69
|
+
* (it can freeze any account of the mint), a Token-2022 pause (`PausableConfig`: its authority stops every transfer), a
|
|
70
|
+
* transfer hook (its program runs on every transfer and can refuse one), a permanent delegate (it can move anyone's
|
|
71
|
+
* tokens of the mint away). Each is [{ power, holder }], `holder` the address that has it. Empty for a plain mint.
|
|
72
|
+
*/
|
|
73
|
+
export function mintPowers({ owner, data }) {
|
|
74
|
+
const out = [];
|
|
75
|
+
if (owner !== TOKEN_PROGRAM && owner !== TOKEN_2022_PROGRAM) return out;
|
|
76
|
+
const option = (at) => (data.length >= at + 36 && readUintLE(data, at, 4) === 1n ? encodeBase58(data.subarray(at + 4, at + 36)) : null);
|
|
77
|
+
const freezer = option(46);
|
|
78
|
+
if (freezer) out.push({ power: 'freeze', holder: freezer });
|
|
79
|
+
if (owner !== TOKEN_2022_PROGRAM || data.length <= TOKEN_ACCOUNT_BYTES || data[TOKEN_ACCOUNT_BYTES] !== ACCOUNT_TYPE_MINT) return out;
|
|
80
|
+
const address = (value, at) => {
|
|
81
|
+
const a = value && value.length >= at + 32 ? encodeBase58(value.subarray(at, at + 32)) : null;
|
|
82
|
+
return a === NO_ADDRESS ? null : a;
|
|
83
|
+
};
|
|
84
|
+
const pause = extension(data, EXTENSION.PausableConfig);
|
|
85
|
+
if (pause) out.push({ power: 'pause', holder: address(pause, 0), paused: pause.length > 32 && pause[32] !== 0 });
|
|
86
|
+
const hook = hookProgramOf(data);
|
|
87
|
+
if (hook) out.push({ power: 'hook', holder: hook, authority: address(extension(data, EXTENSION.TransferHook), 0) });
|
|
88
|
+
const delegate = address(extension(data, EXTENSION.PermanentDelegate), 0);
|
|
89
|
+
if (delegate) out.push({ power: 'delegate', holder: delegate });
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Whether a token account requires a memo on every transfer into it. */
|
|
94
|
+
export function requiresMemo(accountData) {
|
|
95
|
+
const value = accountData.length > TOKEN_ACCOUNT_BYTES ? extension(accountData, EXTENSION.MemoTransfer) : null;
|
|
96
|
+
return Boolean(value?.length && value[0] !== 0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The hook's list of extra accounts for this mint. */
|
|
100
|
+
export const validationAddress = (mint, hookProgram) => pdaOf([utf8('extra-account-metas'), addressBytes(mint)], hookProgram);
|
|
101
|
+
|
|
102
|
+
/** The ExtraAccountMeta entries of an "extra-account-metas" account: [{ discriminator, config, isSigner, isWritable }]. */
|
|
103
|
+
export function readExtraMetas(data) {
|
|
104
|
+
let at = 0;
|
|
105
|
+
while (at + 12 <= data.length) {
|
|
106
|
+
const length = Number(readUintLE(data, at + 8, 4));
|
|
107
|
+
if (at + 12 + length > data.length) throw new RangeError('extra account entry runs past the data');
|
|
108
|
+
const isExecute = EXECUTE_DISCRIMINATOR.every((b, i) => data[at + i] === b);
|
|
109
|
+
if (isExecute) {
|
|
110
|
+
if (length < 4) throw new RangeError('extra account list is missing its count');
|
|
111
|
+
const count = Number(readUintLE(data, at + 12, 4));
|
|
112
|
+
if (4 + count * META_BYTES > length) throw new RangeError('extra account list runs past its entry');
|
|
113
|
+
return Array.from({ length: count }, (_, i) => {
|
|
114
|
+
const o = at + 16 + i * META_BYTES;
|
|
115
|
+
if (o + META_BYTES > at + 12 + length) throw new RangeError('extra account list runs past its entry');
|
|
116
|
+
return { discriminator: data[o], config: data.subarray(o + 1, o + 33), isSigner: data[o + 33] !== 0, isWritable: data[o + 34] !== 0 };
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
at += 12 + length;
|
|
120
|
+
}
|
|
121
|
+
throw new Error('The hook’s account list has no entry for transfers.');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The seeds a PDA entry packs into its 32 bytes: [{ literal } | { ixData: [index, length] } | { key: index } | { data: [account, at, length] }]. */
|
|
125
|
+
function unpackSeeds(config) {
|
|
126
|
+
const seeds = [];
|
|
127
|
+
for (let at = 0; at < config.length && config[at] !== 0; ) {
|
|
128
|
+
const kind = config[at];
|
|
129
|
+
if (kind === 1) {
|
|
130
|
+
seeds.push({ literal: checkedSlice(config, at + 2, config[at + 1], 'literal seed') });
|
|
131
|
+
at += 2 + config[at + 1];
|
|
132
|
+
} else if (kind === 2) {
|
|
133
|
+
checkedSlice(config, at, 3, 'instruction seed');
|
|
134
|
+
seeds.push({ ixData: [config[at + 1], config[at + 2]] });
|
|
135
|
+
at += 3;
|
|
136
|
+
} else if (kind === 3) {
|
|
137
|
+
checkedSlice(config, at, 2, 'account key seed');
|
|
138
|
+
seeds.push({ key: config[at + 1] });
|
|
139
|
+
at += 2;
|
|
140
|
+
} else if (kind === 4) {
|
|
141
|
+
checkedSlice(config, at, 4, 'account data seed');
|
|
142
|
+
seeds.push({ data: [config[at + 1], config[at + 2], config[at + 3]] });
|
|
143
|
+
at += 4;
|
|
144
|
+
} else throw new Error(`The hook asks for an account by a seed this kit does not know (${kind}).`);
|
|
145
|
+
}
|
|
146
|
+
return seeds;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The extra accounts the hook names for one transfer: { source, destination, authority, amount } (amount a bigint).
|
|
151
|
+
* `read(address)` returns an account's data (or null), for seeds taken from account data.
|
|
152
|
+
*/
|
|
153
|
+
async function resolveFor(mint, hookProgram, validation, metas, transfer, read) {
|
|
154
|
+
const keys = [transfer.source, mint, transfer.destination, transfer.authority, validation];
|
|
155
|
+
const ixData = concat(EXECUTE_DISCRIMINATOR, uintLE(transfer.amount ?? 0n, 8));
|
|
156
|
+
const out = [];
|
|
157
|
+
for (const m of metas) {
|
|
158
|
+
if (m.isSigner) throw new Error('This transfer hook requires an additional signature.');
|
|
159
|
+
let address;
|
|
160
|
+
if (m.discriminator === 0) address = encodeBase58(m.config);
|
|
161
|
+
else if (m.discriminator === 1 || m.discriminator >= 128) {
|
|
162
|
+
const program = m.discriminator === 1 ? hookProgram : keys[m.discriminator - 128];
|
|
163
|
+
const seeds = [];
|
|
164
|
+
for (const s of unpackSeeds(m.config)) {
|
|
165
|
+
if (s.literal) seeds.push(s.literal);
|
|
166
|
+
else if (s.ixData) seeds.push(checkedSlice(ixData, s.ixData[0], s.ixData[1], 'instruction seed'));
|
|
167
|
+
else if (s.key !== undefined) seeds.push(addressBytes(keys[s.key]));
|
|
168
|
+
else {
|
|
169
|
+
const data = await read(keys[s.data[0]]);
|
|
170
|
+
if (!data) throw new Error(`The hook needs data from ${keys[s.data[0]]}, which does not exist yet.`);
|
|
171
|
+
seeds.push(checkedSlice(data, s.data[1], s.data[2], 'account data seed'));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (!program) throw new Error('The hook names a program by an account it does not have.');
|
|
175
|
+
address = pdaOf(seeds, program);
|
|
176
|
+
} else throw new Error(`The hook asks for an account in a way this kit does not resolve (${m.discriminator}).`);
|
|
177
|
+
if (!address) throw new Error('The hook asks for an account this transfer does not have.');
|
|
178
|
+
keys.push(address);
|
|
179
|
+
out.push(meta(address, false, m.isWritable));
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Whether the hook's list depends on which transfer it is (an account, the amount or account data). */
|
|
185
|
+
const perTransfer = (metas) =>
|
|
186
|
+
metas.some((m) => m.discriminator >= 128 || (m.discriminator === 1 && unpackSeeds(m.config).some((s) => !s.literal && !(s.key === 1 || s.key === 4))));
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The accounts to append after an instruction's own remaining accounts so every transfer of `mint` it makes can run:
|
|
190
|
+
* [] for a classic mint; the Memo program, and for a hooked mint the hook's accounts, for a Token-2022 one.
|
|
191
|
+
* `transfers`: the transfers the instruction makes ({ source, destination, authority, amount }), needed only when the
|
|
192
|
+
* hook's list depends on them; the accounts of all of them are passed. `rpc` reads the mint and the hook's list.
|
|
193
|
+
*/
|
|
194
|
+
export async function transferExtras(rpc, mint, transfers = []) {
|
|
195
|
+
const [mintAccount] = await rpc.accounts([mint]);
|
|
196
|
+
if (!mintAccount) throw new Error(`There is no token ${mint} on this network.`);
|
|
197
|
+
if (mintAccount.owner === TOKEN_PROGRAM) return [];
|
|
198
|
+
if (mintAccount.owner !== TOKEN_2022_PROGRAM) throw new Error('The mint is not owned by a supported token program.');
|
|
199
|
+
const extras = [meta(MEMO_PROGRAM, false, false)];
|
|
200
|
+
const hookProgram = hookProgramOf(mintAccount.data);
|
|
201
|
+
if (!hookProgram) return extras;
|
|
202
|
+
const validation = validationAddress(mint, hookProgram);
|
|
203
|
+
const [list] = await rpc.accounts([validation]);
|
|
204
|
+
extras.push(meta(hookProgram, false, false), meta(validation, false, false));
|
|
205
|
+
if (!list) throw new Error('The transfer hook account list is missing.');
|
|
206
|
+
if (list.owner !== hookProgram) throw new Error('The transfer hook account list has the wrong owner.');
|
|
207
|
+
const metas = readExtraMetas(list.data);
|
|
208
|
+
const read = async (address) => (await rpc.accounts([address]))[0]?.data ?? null;
|
|
209
|
+
const cases = perTransfer(metas) ? transfers : [{ source: NO_ADDRESS, destination: NO_ADDRESS, authority: NO_ADDRESS, amount: 0n }];
|
|
210
|
+
if (!cases.length) throw new Error('This token’s transfer hook needs to know each transfer; pass them.');
|
|
211
|
+
for (const transfer of cases) for (const m of await resolveFor(mint, hookProgram, validation, metas, transfer, read)) extras.push(m);
|
|
212
|
+
// One entry per address; writable if any transfer needs it writable.
|
|
213
|
+
const merged = new Map();
|
|
214
|
+
for (const m of extras) merged.set(m.pubkey, merged.has(m.pubkey) ? { ...m, isWritable: m.isWritable || merged.get(m.pubkey).isWritable } : m);
|
|
215
|
+
return [...merged.values()];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Length of a newly created associated account, including Token-2022's immutable owner. */
|
|
219
|
+
export function associatedAccountSize(mintAccount) {
|
|
220
|
+
if (mintAccount?.owner === TOKEN_PROGRAM) return TOKEN_ACCOUNT_BYTES;
|
|
221
|
+
if (mintAccount?.owner !== TOKEN_2022_PROGRAM) throw new Error('The mint is not owned by a supported token program.');
|
|
222
|
+
const data = mintAccount.data;
|
|
223
|
+
if (data.length !== MINT_BYTES && (data.length < TOKEN_ACCOUNT_BYTES + 1 || data[TOKEN_ACCOUNT_BYTES] !== ACCOUNT_TYPE_MINT)) throw new Error('Invalid Token-2022 mint layout.');
|
|
224
|
+
const required = new Map([[7, 0]]); // ImmutableOwner, set by the associated token program.
|
|
225
|
+
for (let at = TOKEN_ACCOUNT_BYTES + 1; at + 4 <= data.length;) {
|
|
226
|
+
const kind = Number(readUintLE(data, at, 2));
|
|
227
|
+
const length = Number(readUintLE(data, at + 2, 2));
|
|
228
|
+
if (kind === 0) break;
|
|
229
|
+
if (at + 4 + length > data.length || kind > 27) throw new Error('Unsupported or truncated mint extension.');
|
|
230
|
+
if (kind === 1) required.set(2, 8); // TransferFeeAmount
|
|
231
|
+
if (kind === 9) required.set(13, 0); // NonTransferableAccount
|
|
232
|
+
if (kind === 14) required.set(15, 1); // TransferHookAccount
|
|
233
|
+
if (kind === 26) required.set(27, 0); // PausableAccount
|
|
234
|
+
at += 4 + length;
|
|
235
|
+
}
|
|
236
|
+
const size = TOKEN_ACCOUNT_BYTES + 1 + [...required.values()].reduce((n, length) => n + 4 + length, 0);
|
|
237
|
+
return size === 355 ? size + 2 : size;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Shared browser/keeper resolution for all transfers of one mint, preserving role accounts before extras. */
|
|
241
|
+
export async function resolveTransfers(rpc, mint, transfers = []) {
|
|
242
|
+
const [account] = await rpc.accounts([mint]);
|
|
243
|
+
if (!account) throw new Error(`There is no token ${mint} on this network.`);
|
|
244
|
+
const accountSize = associatedAccountSize(account);
|
|
245
|
+
const cached = { accounts: async (keys) => keys.length === 1 && keys[0] === mint ? [account] : rpc.accounts(keys) };
|
|
246
|
+
return { tokenProgram: account.owner, accountSize, extraAccounts: await transferExtras(cached, mint, transfers) };
|
|
247
|
+
}
|