@xgamesfun/solana 0.20.1 → 0.25.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/README.md +95 -52
- package/bin/xsolana.mjs +23 -8
- package/examples/blind-forge.json +2 -4
- package/examples/coin-flip-arena.json +4 -12
- package/examples/duel-arena.json +1 -4
- package/examples/elements-set.json +2 -9
- package/examples/expedition-guild.json +1 -8
- package/examples/fair-presale.json +1 -3
- package/examples/guild-missions.json +3 -10
- package/examples/halving-seasons.json +4 -9
- package/examples/hero-trials.json +15 -6
- package/examples/holder-den.json +38 -0
- package/examples/holders-drop.json +105 -0
- package/examples/ideas.json +788 -788
- package/examples/match-predictions.json +24 -0
- package/examples/merge-2048.json +5 -5
- package/examples/nft-raffle.json +1 -0
- package/examples/pack-league.json +108 -0
- package/examples/paper-hands-farm.json +4 -16
- package/examples/partner-vault.json +20 -7
- package/examples/sacrifice-raffle.json +2 -10
- package/examples/shelter-fair.json +1 -0
- package/examples/stakers-draw.json +4 -15
- package/examples/star-fleet.json +1 -8
- package/examples/starforce.json +6 -6
- package/examples/stride-sneakers.json +164 -0
- package/examples/trade-in-shop.json +2 -3
- package/js/approve/details.mjs +122 -52
- package/js/approve/ed25519.mjs +5 -1
- package/js/approve/live-check.mjs +83 -26
- package/js/approve/page.html +217 -78
- package/js/approve/page.js +43 -15
- package/js/approve/server.mjs +290 -73
- package/js/chain/base58.js +7 -1
- package/js/chain/borsh.js +24 -11
- package/js/chain/bytes.js +11 -8
- package/js/chain/core.js +35 -7
- package/js/chain/curve.js +1 -1
- package/js/chain/lookup.js +124 -36
- package/js/chain/mint.js +6 -2
- package/js/chain/pda.js +2 -2
- package/js/chain/programs.js +6 -6
- package/js/chain/rpc.js +44 -16
- package/js/chain/token.js +13 -2
- package/js/chain/token2022.js +55 -16
- package/js/chain/tx.js +32 -22
- package/js/client/accounts.js +653 -131
- package/js/client/compressed.js +4 -3
- package/js/client/grant.js +267 -83
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +2406 -2176
- package/js/client/index.js +1 -0
- package/js/client/ix.js +3059 -664
- package/js/client/pda.js +39 -33
- package/js/client/policy.js +52 -24
- package/js/client/random.js +70 -0
- package/js/client/sizes.js +414 -16
- package/js/client/traits.js +14 -5
- package/js/connect/connect.mjs +59 -17
- package/js/economy/check.mjs +4153 -725
- package/js/economy/cost.mjs +403 -64
- package/js/economy/examples.mjs +176 -35
- package/js/economy/explain.mjs +466 -111
- package/js/economy/guide.mjs +296 -72
- package/js/economy/legal.mjs +12 -6
- package/js/economy/live.mjs +83 -19
- package/js/economy/model.mjs +462 -124
- package/js/economy/plan.mjs +1306 -219
- package/js/economy/schema.json +390 -484
- package/js/economy/secrets.mjs +17 -7
- package/js/economy/simulate.mjs +883 -223
- package/js/economy/txsize.mjs +15 -3
- package/js/economy/validate.mjs +61 -24
- package/js/mcp/server.mjs +42 -14
- package/js/mcp/tools.mjs +307 -88
- package/js/play/index.js +2864 -619
- package/js/play/keys.js +193 -0
- package/js/results/index.js +223 -30
- package/js/results/server.js +230 -0
- package/js/results/wire.js +64 -16
- package/package.json +6 -4
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +39 -21
- package/skills/xsolana-economy/references/after-launch.md +58 -38
- package/skills/xsolana-economy/references/apps.md +97 -50
- package/skills/xsolana-economy/references/sentence-to-spec.md +148 -121
- package/skills/xsolana-economy/references/telling-the-person.md +61 -17
- package/types/chain/core.d.ts +4 -2
- package/types/chain/lookup.d.ts +5 -2
- package/types/chain/rpc.d.ts +3 -1
- package/types/client/accounts.d.ts +217 -56
- package/types/client/grant.d.ts +72 -27
- package/types/client/idl/loot.d.ts +602 -549
- package/types/client/index.d.ts +1 -0
- package/types/client/ix.d.ts +3884 -247
- package/types/client/pda.d.ts +22 -16
- package/types/client/policy.d.ts +39 -14
- package/types/client/random.d.ts +26 -0
- package/types/client/sizes.d.ts +93 -5
- package/types/economy/check.d.mts +5 -10
- package/types/economy/cost.d.mts +2 -1
- package/types/economy/guide.d.mts +5 -0
- package/types/economy/live.d.mts +16 -1
- package/types/economy/model.d.mts +151 -38
- package/types/economy/plan.d.mts +237 -194
- package/types/economy/simulate.d.mts +21 -13
- package/types/mcp/tools.d.mts +21 -13
- package/types/play/index.d.ts +453 -53
- package/types/play/keys.d.ts +51 -0
- package/types/results/index.d.ts +7 -6
- package/types/results/server.d.ts +126 -0
- package/types/results/wire.d.ts +1 -1
package/js/chain/rpc.js
CHANGED
|
@@ -29,7 +29,7 @@ export class RpcError extends Error {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
const sleep =
|
|
32
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
33
33
|
|
|
34
34
|
/** The first block of each public Solana cluster: it names the cluster whatever URL reaches it. */
|
|
35
35
|
export const GENESIS = Object.freeze({
|
|
@@ -38,7 +38,8 @@ export const GENESIS = Object.freeze({
|
|
|
38
38
|
testnet: '4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY',
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
const DEVNET_ONLY =
|
|
41
|
+
const DEVNET_ONLY =
|
|
42
|
+
'loot runs on devnet only: mainnet waits for an audit. Unset XSOLANA_RPC_URL, or point it at a devnet RPC.';
|
|
42
43
|
|
|
43
44
|
/** Refuses an RPC URL that names mainnet. Returns the URL. */
|
|
44
45
|
export function notMainnet(url) {
|
|
@@ -56,16 +57,18 @@ export async function checkCluster(rpc) {
|
|
|
56
57
|
if (genesis === GENESIS.testnet) throw new RpcError(`${rpc.url ?? 'This RPC'} is on testnet. ${DEVNET_ONLY}`);
|
|
57
58
|
return genesis;
|
|
58
59
|
}
|
|
59
|
-
const decodeAccount =
|
|
60
|
+
const decodeAccount = raw =>
|
|
60
61
|
raw ? { owner: raw.owner, lamports: raw.lamports, data: fromBase64(raw.data[0]), executable: raw.executable } : null;
|
|
61
62
|
|
|
62
|
-
/** The program's own words for a failed run (`AnchorError … Error Message: …`), else the last error log line.
|
|
63
|
+
/** The program's own words for a failed run (`AnchorError … Error Message: …`), else the last error log line. A program
|
|
64
|
+
* built without its errors' text logs the error's name as its message: then the name alone (the client's IDL has
|
|
65
|
+
* the sentence: `loot.lootError`). */
|
|
63
66
|
export function programError(err, logs) {
|
|
64
|
-
const anchor = logs?.find(
|
|
67
|
+
const anchor = logs?.find(line => line.includes('AnchorError'));
|
|
65
68
|
const said = anchor ? /Error Message: (.+?)\.?$/.exec(anchor)?.[1] : undefined;
|
|
66
69
|
const code = anchor ? /Error Code: (\w+)/.exec(anchor)?.[1] : undefined;
|
|
67
|
-
const last = logs?.filter(
|
|
68
|
-
if (said) return code ? `${said} (${code})` : said;
|
|
70
|
+
const last = logs?.filter(l => /error|failed|insufficient/i.test(l)).at(-1);
|
|
71
|
+
if (said) return code && code !== said ? `${said} (${code})` : said;
|
|
69
72
|
return last ?? (typeof err === 'string' ? err : JSON.stringify(err));
|
|
70
73
|
}
|
|
71
74
|
|
|
@@ -115,7 +118,9 @@ export function rpcClient(url, { fetchImpl = globalThis.fetch, sleepImpl = sleep
|
|
|
115
118
|
// A send whose preflight failed ("Transaction simulation failed: … custom program error: 0x17b5") carries the
|
|
116
119
|
// logs, where the program says why in words.
|
|
117
120
|
const logs = body?.error?.data?.logs;
|
|
118
|
-
const said = logs?.length
|
|
121
|
+
const said = logs?.length
|
|
122
|
+
? `The network refused this in a dry run: ${programError(body.error.data.err, logs)}`
|
|
123
|
+
: message;
|
|
119
124
|
throw new RpcError(said, { code: body?.error?.code, logs });
|
|
120
125
|
}
|
|
121
126
|
return body.result;
|
|
@@ -126,7 +131,14 @@ export function rpcClient(url, { fetchImpl = globalThis.fetch, sleepImpl = sleep
|
|
|
126
131
|
const out = [];
|
|
127
132
|
for (let i = 0; i < addresses.length; i += ACCOUNTS_PER_CALL) {
|
|
128
133
|
const batch = addresses.slice(i, i + ACCOUNTS_PER_CALL);
|
|
129
|
-
const r = await call('getMultipleAccounts', [
|
|
134
|
+
const r = await call('getMultipleAccounts', [
|
|
135
|
+
batch,
|
|
136
|
+
{
|
|
137
|
+
encoding: 'base64',
|
|
138
|
+
commitment,
|
|
139
|
+
...(minContextSlot != null ? { minContextSlot: Number(minContextSlot) } : {}),
|
|
140
|
+
},
|
|
141
|
+
]);
|
|
130
142
|
out.push(...r.value.map(decodeAccount));
|
|
131
143
|
}
|
|
132
144
|
return out;
|
|
@@ -170,7 +182,7 @@ export function rpcClient(url, { fetchImpl = globalThis.fetch, sleepImpl = sleep
|
|
|
170
182
|
},
|
|
171
183
|
async programAccounts(program, filters) {
|
|
172
184
|
const r = await call('getProgramAccounts', [program, { encoding: 'base64', commitment: 'confirmed', filters }]);
|
|
173
|
-
return r.map(
|
|
185
|
+
return r.map(x => ({ pubkey: x.pubkey, account: decodeAccount(x.account) }));
|
|
174
186
|
},
|
|
175
187
|
/**
|
|
176
188
|
* Dry-run a wire transaction (empty signature slots are fine). Throws with the program's reason when it fails.
|
|
@@ -188,11 +200,21 @@ export function rpcClient(url, { fetchImpl = globalThis.fetch, sleepImpl = sleep
|
|
|
188
200
|
},
|
|
189
201
|
]);
|
|
190
202
|
const value = r.value;
|
|
191
|
-
if (value.err)
|
|
192
|
-
|
|
203
|
+
if (value.err)
|
|
204
|
+
throw new RpcError(`The network refused this in a dry run: ${programError(value.err, value.logs)}`, {
|
|
205
|
+
logs: value.logs,
|
|
206
|
+
});
|
|
207
|
+
return {
|
|
208
|
+
unitsConsumed: value.unitsConsumed ?? null,
|
|
209
|
+
logs: value.logs ?? [],
|
|
210
|
+
accounts: (value.accounts ?? []).map(decodeAccount),
|
|
211
|
+
};
|
|
193
212
|
},
|
|
194
213
|
async send(wire) {
|
|
195
|
-
return call('sendTransaction', [
|
|
214
|
+
return call('sendTransaction', [
|
|
215
|
+
toBase64(wire),
|
|
216
|
+
{ encoding: 'base64', preflightCommitment: 'confirmed', maxRetries: 3 },
|
|
217
|
+
]);
|
|
196
218
|
},
|
|
197
219
|
signatureStatus,
|
|
198
220
|
/** Wait until a sent transaction is confirmed; throws with the program's reason when it failed. */
|
|
@@ -201,13 +223,19 @@ export function rpcClient(url, { fetchImpl = globalThis.fetch, sleepImpl = sleep
|
|
|
201
223
|
for (let poll = 0; Date.now() < deadline; poll++) {
|
|
202
224
|
const status = await signatureStatus(signature);
|
|
203
225
|
if (status?.err) {
|
|
204
|
-
const tx = await call('getTransaction', [
|
|
205
|
-
|
|
226
|
+
const tx = await call('getTransaction', [
|
|
227
|
+
signature,
|
|
228
|
+
{ commitment: 'confirmed', maxSupportedTransactionVersion: 0 },
|
|
229
|
+
]).catch(() => null);
|
|
230
|
+
throw new RpcError(`Transaction ${signature} failed: ${programError(status.err, tx?.meta?.logMessages)}`, {
|
|
231
|
+
logs: tx?.meta?.logMessages,
|
|
232
|
+
});
|
|
206
233
|
}
|
|
207
234
|
if (status?.confirmationStatus === 'confirmed' || status?.confirmationStatus === 'finalized') return signature;
|
|
208
235
|
if (lastValidBlockHeight !== null && poll % 5 === 4) {
|
|
209
236
|
const height = await call('getBlockHeight', [{ commitment: 'confirmed' }]).catch(() => 0);
|
|
210
|
-
if (height > lastValidBlockHeight)
|
|
237
|
+
if (height > lastValidBlockHeight)
|
|
238
|
+
throw new RpcError(`Transaction ${signature} expired before it landed. Try again.`);
|
|
211
239
|
}
|
|
212
240
|
await sleepImpl(CONFIRM_POLL_MS);
|
|
213
241
|
}
|
package/js/chain/token.js
CHANGED
|
@@ -28,7 +28,15 @@ export function createAccountWithSeedIx({ from, address, seed, lamports, space,
|
|
|
28
28
|
return {
|
|
29
29
|
programId: SYSTEM_PROGRAM,
|
|
30
30
|
keys: [meta(from, true, true), meta(address, false, true)],
|
|
31
|
-
data: concat(
|
|
31
|
+
data: concat(
|
|
32
|
+
uintLE(3, 4),
|
|
33
|
+
addressBytes(from),
|
|
34
|
+
uintLE(text.length, 8),
|
|
35
|
+
text,
|
|
36
|
+
uintLE(lamports, 8),
|
|
37
|
+
uintLE(space, 8),
|
|
38
|
+
addressBytes(owner),
|
|
39
|
+
),
|
|
32
40
|
};
|
|
33
41
|
}
|
|
34
42
|
|
|
@@ -81,5 +89,8 @@ export const transferCheckedIx = ({ from, mint, to, owner, amount, decimals, tok
|
|
|
81
89
|
export const setAuthorityIx = ({ account, current, kind = 0, newAuthority, tokenProgram = TOKEN_PROGRAM }) => ({
|
|
82
90
|
programId: tokenProgram,
|
|
83
91
|
keys: [meta(account, false, true), meta(current, true, false)],
|
|
84
|
-
data: concat(
|
|
92
|
+
data: concat(
|
|
93
|
+
Uint8Array.of(6, kind),
|
|
94
|
+
newAuthority ? concat(Uint8Array.of(1), addressBytes(newAuthority)) : Uint8Array.of(0),
|
|
95
|
+
),
|
|
85
96
|
});
|
package/js/chain/token2022.js
CHANGED
|
@@ -19,11 +19,18 @@ export const MEMO_PROGRAM = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr';
|
|
|
19
19
|
export const EXECUTE_DISCRIMINATOR = Uint8Array.of(105, 37, 101, 197, 75, 251, 102, 26);
|
|
20
20
|
|
|
21
21
|
const ACCOUNT_TYPE_MINT = 1;
|
|
22
|
-
const EXTENSION = {
|
|
22
|
+
const EXTENSION = {
|
|
23
|
+
DefaultAccountState: 6,
|
|
24
|
+
MemoTransfer: 8,
|
|
25
|
+
PermanentDelegate: 12,
|
|
26
|
+
TransferHook: 14,
|
|
27
|
+
PausableConfig: 26,
|
|
28
|
+
};
|
|
23
29
|
const META_BYTES = 35;
|
|
24
30
|
|
|
25
31
|
function checkedSlice(data, at, length, what) {
|
|
26
|
-
if (!Number.isInteger(at) || !Number.isInteger(length) || at < 0 || length < 0 || at + length > data.length)
|
|
32
|
+
if (!Number.isInteger(at) || !Number.isInteger(length) || at < 0 || length < 0 || at + length > data.length)
|
|
33
|
+
throw new RangeError(`${what} runs past the data`);
|
|
27
34
|
return data.subarray(at, at + length);
|
|
28
35
|
}
|
|
29
36
|
|
|
@@ -57,7 +64,12 @@ export function hookProgramOf(mintData) {
|
|
|
57
64
|
* each reads the setting once, as it opens, and a refund or payout opened into a frozen account would never land.
|
|
58
65
|
*/
|
|
59
66
|
export function mayStartFrozen({ owner, data }) {
|
|
60
|
-
if (
|
|
67
|
+
if (
|
|
68
|
+
owner !== TOKEN_2022_PROGRAM ||
|
|
69
|
+
data.length <= TOKEN_ACCOUNT_BYTES ||
|
|
70
|
+
data[TOKEN_ACCOUNT_BYTES] !== ACCOUNT_TYPE_MINT
|
|
71
|
+
)
|
|
72
|
+
return false;
|
|
61
73
|
const state = extension(data, EXTENSION.DefaultAccountState);
|
|
62
74
|
if (!state) return false;
|
|
63
75
|
const freezer = data.length >= 50 && readUintLE(data, 46, 4) !== 0n;
|
|
@@ -73,10 +85,16 @@ export function mayStartFrozen({ owner, data }) {
|
|
|
73
85
|
export function mintPowers({ owner, data }) {
|
|
74
86
|
const out = [];
|
|
75
87
|
if (owner !== TOKEN_PROGRAM && owner !== TOKEN_2022_PROGRAM) return out;
|
|
76
|
-
const option =
|
|
88
|
+
const option = at =>
|
|
89
|
+
data.length >= at + 36 && readUintLE(data, at, 4) === 1n ? encodeBase58(data.subarray(at + 4, at + 36)) : null;
|
|
77
90
|
const freezer = option(46);
|
|
78
91
|
if (freezer) out.push({ power: 'freeze', holder: freezer });
|
|
79
|
-
if (
|
|
92
|
+
if (
|
|
93
|
+
owner !== TOKEN_2022_PROGRAM ||
|
|
94
|
+
data.length <= TOKEN_ACCOUNT_BYTES ||
|
|
95
|
+
data[TOKEN_ACCOUNT_BYTES] !== ACCOUNT_TYPE_MINT
|
|
96
|
+
)
|
|
97
|
+
return out;
|
|
80
98
|
const address = (value, at) => {
|
|
81
99
|
const a = value && value.length >= at + 32 ? encodeBase58(value.subarray(at, at + 32)) : null;
|
|
82
100
|
return a === NO_ADDRESS ? null : a;
|
|
@@ -97,7 +115,8 @@ export function requiresMemo(accountData) {
|
|
|
97
115
|
}
|
|
98
116
|
|
|
99
117
|
/** The hook's list of extra accounts for this mint. */
|
|
100
|
-
export const validationAddress = (mint, hookProgram) =>
|
|
118
|
+
export const validationAddress = (mint, hookProgram) =>
|
|
119
|
+
pdaOf([utf8('extra-account-metas'), addressBytes(mint)], hookProgram);
|
|
101
120
|
|
|
102
121
|
/** The ExtraAccountMeta entries of an "extra-account-metas" account: [{ discriminator, config, isSigner, isWritable }]. */
|
|
103
122
|
export function readExtraMetas(data) {
|
|
@@ -113,7 +132,12 @@ export function readExtraMetas(data) {
|
|
|
113
132
|
return Array.from({ length: count }, (_, i) => {
|
|
114
133
|
const o = at + 16 + i * META_BYTES;
|
|
115
134
|
if (o + META_BYTES > at + 12 + length) throw new RangeError('extra account list runs past its entry');
|
|
116
|
-
return {
|
|
135
|
+
return {
|
|
136
|
+
discriminator: data[o],
|
|
137
|
+
config: data.subarray(o + 1, o + 33),
|
|
138
|
+
isSigner: data[o + 33] !== 0,
|
|
139
|
+
isWritable: data[o + 34] !== 0,
|
|
140
|
+
};
|
|
117
141
|
});
|
|
118
142
|
}
|
|
119
143
|
at += 12 + length;
|
|
@@ -124,7 +148,7 @@ export function readExtraMetas(data) {
|
|
|
124
148
|
/** The seeds a PDA entry packs into its 32 bytes: [{ literal } | { ixData: [index, length] } | { key: index } | { data: [account, at, length] }]. */
|
|
125
149
|
function unpackSeeds(config) {
|
|
126
150
|
const seeds = [];
|
|
127
|
-
for (let at = 0; at < config.length && config[at] !== 0;
|
|
151
|
+
for (let at = 0; at < config.length && config[at] !== 0;) {
|
|
128
152
|
const kind = config[at];
|
|
129
153
|
if (kind === 1) {
|
|
130
154
|
seeds.push({ literal: checkedSlice(config, at + 2, config[at + 1], 'literal seed') });
|
|
@@ -182,8 +206,12 @@ async function resolveFor(mint, hookProgram, validation, metas, transfer, read)
|
|
|
182
206
|
}
|
|
183
207
|
|
|
184
208
|
/** Whether the hook's list depends on which transfer it is (an account, the amount or account data). */
|
|
185
|
-
const perTransfer =
|
|
186
|
-
metas.some(
|
|
209
|
+
const perTransfer = metas =>
|
|
210
|
+
metas.some(
|
|
211
|
+
m =>
|
|
212
|
+
m.discriminator >= 128 ||
|
|
213
|
+
(m.discriminator === 1 && unpackSeeds(m.config).some(s => !s.literal && !(s.key === 1 || s.key === 4))),
|
|
214
|
+
);
|
|
187
215
|
|
|
188
216
|
/**
|
|
189
217
|
* The accounts to append after an instruction's own remaining accounts so every transfer of `mint` it makes can run:
|
|
@@ -205,13 +233,20 @@ export async function transferExtras(rpc, mint, transfers = []) {
|
|
|
205
233
|
if (!list) throw new Error('The transfer hook account list is missing.');
|
|
206
234
|
if (list.owner !== hookProgram) throw new Error('The transfer hook account list has the wrong owner.');
|
|
207
235
|
const metas = readExtraMetas(list.data);
|
|
208
|
-
const read = async
|
|
209
|
-
const cases = perTransfer(metas)
|
|
236
|
+
const read = async address => (await rpc.accounts([address]))[0]?.data ?? null;
|
|
237
|
+
const cases = perTransfer(metas)
|
|
238
|
+
? transfers
|
|
239
|
+
: [{ source: NO_ADDRESS, destination: NO_ADDRESS, authority: NO_ADDRESS, amount: 0n }];
|
|
210
240
|
if (!cases.length) throw new Error('This token’s transfer hook needs to know each transfer; pass them.');
|
|
211
|
-
for (const transfer of cases)
|
|
241
|
+
for (const transfer of cases)
|
|
242
|
+
for (const m of await resolveFor(mint, hookProgram, validation, metas, transfer, read)) extras.push(m);
|
|
212
243
|
// One entry per address; writable if any transfer needs it writable.
|
|
213
244
|
const merged = new Map();
|
|
214
|
-
for (const m of extras)
|
|
245
|
+
for (const m of extras)
|
|
246
|
+
merged.set(
|
|
247
|
+
m.pubkey,
|
|
248
|
+
merged.has(m.pubkey) ? { ...m, isWritable: m.isWritable || merged.get(m.pubkey).isWritable } : m,
|
|
249
|
+
);
|
|
215
250
|
return [...merged.values()];
|
|
216
251
|
}
|
|
217
252
|
|
|
@@ -220,7 +255,11 @@ export function associatedAccountSize(mintAccount) {
|
|
|
220
255
|
if (mintAccount?.owner === TOKEN_PROGRAM) return TOKEN_ACCOUNT_BYTES;
|
|
221
256
|
if (mintAccount?.owner !== TOKEN_2022_PROGRAM) throw new Error('The mint is not owned by a supported token program.');
|
|
222
257
|
const data = mintAccount.data;
|
|
223
|
-
if (
|
|
258
|
+
if (
|
|
259
|
+
data.length !== MINT_BYTES &&
|
|
260
|
+
(data.length < TOKEN_ACCOUNT_BYTES + 1 || data[TOKEN_ACCOUNT_BYTES] !== ACCOUNT_TYPE_MINT)
|
|
261
|
+
)
|
|
262
|
+
throw new Error('Invalid Token-2022 mint layout.');
|
|
224
263
|
const required = new Map([[7, 0]]); // ImmutableOwner, set by the associated token program.
|
|
225
264
|
for (let at = TOKEN_ACCOUNT_BYTES + 1; at + 4 <= data.length;) {
|
|
226
265
|
const kind = Number(readUintLE(data, at, 2));
|
|
@@ -242,6 +281,6 @@ export async function resolveTransfers(rpc, mint, transfers = []) {
|
|
|
242
281
|
const [account] = await rpc.accounts([mint]);
|
|
243
282
|
if (!account) throw new Error(`There is no token ${mint} on this network.`);
|
|
244
283
|
const accountSize = associatedAccountSize(account);
|
|
245
|
-
const cached = { accounts: async
|
|
284
|
+
const cached = { accounts: async keys => (keys.length === 1 && keys[0] === mint ? [account] : rpc.accounts(keys)) };
|
|
246
285
|
return { tokenProgram: account.owner, accountSize, extraAccounts: await transferExtras(cached, mint, transfers) };
|
|
247
286
|
}
|
package/js/chain/tx.js
CHANGED
|
@@ -33,14 +33,14 @@ function mergedAccounts(payer, instructions) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/** The order a message lists accounts in: signers first, writable before read-only; payer at the head. */
|
|
36
|
-
const rank =
|
|
36
|
+
const rank = m => (m.isSigner ? 0 : 2) + (m.isWritable ? 0 : 1);
|
|
37
37
|
|
|
38
38
|
function compileInstructions(instructions, index) {
|
|
39
|
-
return instructions.map(
|
|
39
|
+
return instructions.map(ix =>
|
|
40
40
|
concat(
|
|
41
41
|
Uint8Array.of(index.get(ix.programId)),
|
|
42
42
|
compactU16(ix.keys.length),
|
|
43
|
-
Uint8Array.from(ix.keys.map(
|
|
43
|
+
Uint8Array.from(ix.keys.map(k => index.get(k.pubkey))),
|
|
44
44
|
compactU16(ix.data.length),
|
|
45
45
|
ix.data,
|
|
46
46
|
),
|
|
@@ -57,10 +57,10 @@ export function compileMessage({ payer, blockhash, instructions, version = 'lega
|
|
|
57
57
|
const lookups = [];
|
|
58
58
|
if (version === 0) {
|
|
59
59
|
for (const table of lookupTables) {
|
|
60
|
-
const drain =
|
|
60
|
+
const drain = writable => {
|
|
61
61
|
const indexes = [];
|
|
62
62
|
const keys = [];
|
|
63
|
-
accounts = accounts.filter(
|
|
63
|
+
accounts = accounts.filter(m => {
|
|
64
64
|
if (m.isSigner || m.invoked || m.isWritable !== writable) return true;
|
|
65
65
|
const at = table.addresses.indexOf(m.pubkey);
|
|
66
66
|
if (at < 0) return true;
|
|
@@ -74,30 +74,36 @@ export function compileMessage({ payer, blockhash, instructions, version = 'lega
|
|
|
74
74
|
const w = drain(true);
|
|
75
75
|
const r = drain(false);
|
|
76
76
|
if (w.indexes.length || r.indexes.length)
|
|
77
|
-
lookups.push({
|
|
77
|
+
lookups.push({
|
|
78
|
+
address: table.address,
|
|
79
|
+
writable: w.indexes,
|
|
80
|
+
readonly: r.indexes,
|
|
81
|
+
writableKeys: w.keys,
|
|
82
|
+
readonlyKeys: r.keys,
|
|
83
|
+
});
|
|
78
84
|
}
|
|
79
85
|
}
|
|
80
86
|
const [head, ...rest] = accounts;
|
|
81
87
|
const statics = [head, ...rest.sort((a, b) => rank(a) - rank(b))];
|
|
82
|
-
const loaded = [...lookups.flatMap(
|
|
88
|
+
const loaded = [...lookups.flatMap(l => l.writableKeys), ...lookups.flatMap(l => l.readonlyKeys)];
|
|
83
89
|
const index = new Map([...statics.map((m, i) => [m.pubkey, i]), ...loaded.map((k, i) => [k, statics.length + i])]);
|
|
84
90
|
if (index.size > 256) throw new Error('Too many accounts for one transaction');
|
|
85
|
-
const signers = statics.filter(
|
|
91
|
+
const signers = statics.filter(m => m.isSigner);
|
|
86
92
|
const header = Uint8Array.of(
|
|
87
93
|
signers.length,
|
|
88
|
-
signers.filter(
|
|
89
|
-
statics.filter(
|
|
94
|
+
signers.filter(m => !m.isWritable).length,
|
|
95
|
+
statics.filter(m => !m.isSigner && !m.isWritable).length,
|
|
90
96
|
);
|
|
91
97
|
const body = concat(
|
|
92
98
|
header,
|
|
93
99
|
compactU16(statics.length),
|
|
94
|
-
...statics.map(
|
|
100
|
+
...statics.map(m => addressBytes(m.pubkey)),
|
|
95
101
|
addressBytes(blockhash),
|
|
96
102
|
compactU16(instructions.length),
|
|
97
103
|
...compileInstructions(instructions, index),
|
|
98
104
|
);
|
|
99
105
|
if (version !== 0) return body;
|
|
100
|
-
const tables = lookups.map(
|
|
106
|
+
const tables = lookups.map(l =>
|
|
101
107
|
concat(
|
|
102
108
|
addressBytes(l.address),
|
|
103
109
|
compactU16(l.writable.length),
|
|
@@ -122,13 +128,13 @@ export function readMessage(message) {
|
|
|
122
128
|
at += n;
|
|
123
129
|
return bytes;
|
|
124
130
|
};
|
|
125
|
-
const length =
|
|
131
|
+
const length = what => {
|
|
126
132
|
if (at >= message.length) throw new Error(`Truncated message: ${what} runs past its end`);
|
|
127
133
|
const prefix = readCompactU16(message, at);
|
|
128
134
|
at += prefix.size;
|
|
129
135
|
return prefix.value;
|
|
130
136
|
};
|
|
131
|
-
const address =
|
|
137
|
+
const address = what => encodeBase58(take(32, what));
|
|
132
138
|
let version = 'legacy';
|
|
133
139
|
if (message.length && message[0] & V0_PREFIX) {
|
|
134
140
|
version = message[0] & 0x7f;
|
|
@@ -145,8 +151,8 @@ export function readMessage(message) {
|
|
|
145
151
|
const instructions = [];
|
|
146
152
|
for (let i = 0; i < ixCount; i++) {
|
|
147
153
|
const [programIndex] = take(1, 'an instruction');
|
|
148
|
-
const accountIndexes = Array.from(take(length(
|
|
149
|
-
const data = take(length(
|
|
154
|
+
const accountIndexes = Array.from(take(length("an instruction's accounts"), "an instruction's accounts"));
|
|
155
|
+
const data = take(length("an instruction's data"), "an instruction's data").slice();
|
|
150
156
|
instructions.push({ programIndex, accountIndexes, data });
|
|
151
157
|
}
|
|
152
158
|
const lookups = [];
|
|
@@ -154,8 +160,12 @@ export function readMessage(message) {
|
|
|
154
160
|
const n = length('the lookup table count');
|
|
155
161
|
for (let i = 0; i < n; i++) {
|
|
156
162
|
const tableAddress = address('a lookup table');
|
|
157
|
-
const writable = Array.from(
|
|
158
|
-
|
|
163
|
+
const writable = Array.from(
|
|
164
|
+
take(length("a lookup table's writable indexes"), "a lookup table's writable indexes"),
|
|
165
|
+
);
|
|
166
|
+
const readonly = Array.from(
|
|
167
|
+
take(length("a lookup table's read-only indexes"), "a lookup table's read-only indexes"),
|
|
168
|
+
);
|
|
159
169
|
lookups.push({ address: tableAddress, writable, readonly });
|
|
160
170
|
}
|
|
161
171
|
}
|
|
@@ -173,7 +183,7 @@ export function signersOf(message) {
|
|
|
173
183
|
export function wireTransaction(message, signatures = []) {
|
|
174
184
|
const count = readMessage(message).header.numRequiredSignatures;
|
|
175
185
|
const slots = Array.from({ length: count }, (_, i) => signatures[i] ?? new Uint8Array(SIGNATURE_BYTES));
|
|
176
|
-
if (slots.some(
|
|
186
|
+
if (slots.some(s => s.length !== SIGNATURE_BYTES)) throw new Error('A signature is 64 bytes');
|
|
177
187
|
return concat(compactU16(count), ...slots, message);
|
|
178
188
|
}
|
|
179
189
|
|
|
@@ -203,16 +213,16 @@ export function withSignature(wire, signer, signature) {
|
|
|
203
213
|
}
|
|
204
214
|
|
|
205
215
|
/** The transaction's id: its first signature, in base58. */
|
|
206
|
-
export const transactionId =
|
|
216
|
+
export const transactionId = wire => encodeBase58(readTransaction(wire).signatures[0]);
|
|
207
217
|
|
|
208
218
|
/** Whether a signature slot is still empty. */
|
|
209
|
-
export const isEmptySignature =
|
|
219
|
+
export const isEmptySignature = signature => signature.every(b => b === 0);
|
|
210
220
|
|
|
211
221
|
/** Whether two wire transactions carry the same message. */
|
|
212
222
|
export const sameMessage = (a, b) => equalBytes(readTransaction(a).message, readTransaction(b).message);
|
|
213
223
|
|
|
214
224
|
/** The size this message's transaction takes on the wire. */
|
|
215
|
-
export const transactionSize =
|
|
225
|
+
export const transactionSize = message => wireTransaction(message).length;
|
|
216
226
|
|
|
217
227
|
/** Whether these instructions fit one transaction from this payer. */
|
|
218
228
|
export function fits(input) {
|