@xgamesfun/solana 0.0.0-stage → 0.20.1
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 +108 -0
- package/examples/duel-arena.json +137 -0
- package/examples/elements-set.json +281 -0
- package/examples/event-badges.json +67 -0
- package/examples/expedition-guild.json +161 -0
- package/examples/fair-presale.json +48 -0
- package/examples/falling-drop.json +90 -0
- package/examples/guild-missions.json +144 -0
- package/examples/halving-seasons.json +112 -0
- package/examples/hero-trials.json +96 -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 +56 -0
- package/examples/partner-vault.json +115 -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 +125 -0
- package/examples/star-fleet.json +157 -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 +262 -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 +711 -0
- package/js/chain/base58.js +73 -0
- package/js/chain/borsh.js +261 -0
- package/js/chain/bytes.js +141 -0
- package/js/chain/core.js +169 -0
- package/js/chain/curve.js +52 -0
- package/js/chain/lookup.js +273 -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 +217 -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 +347 -0
- package/js/client/compressed.js +46 -0
- package/js/client/grant.js +240 -0
- package/js/client/idl/loot.js +2 -0
- package/js/client/idl/loot.json +17064 -0
- package/js/client/index.js +9 -0
- package/js/client/ix.js +2156 -0
- package/js/client/pda.js +110 -0
- package/js/client/policy.js +48 -0
- package/js/client/sizes.js +54 -0
- package/js/client/traits.js +44 -0
- package/js/connect/connect.mjs +182 -0
- package/js/economy/check.mjs +1991 -0
- package/js/economy/cost.mjs +170 -0
- package/js/economy/examples.mjs +114 -0
- package/js/economy/explain.mjs +235 -0
- package/js/economy/guide.mjs +231 -0
- package/js/economy/legal.mjs +15 -0
- package/js/economy/live.mjs +107 -0
- package/js/economy/model.mjs +583 -0
- package/js/economy/plan.mjs +887 -0
- package/js/economy/schema.json +2923 -0
- package/js/economy/secrets.mjs +60 -0
- package/js/economy/simulate.mjs +952 -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 +479 -0
- package/js/play/index.js +2109 -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 +105 -0
- package/skills/xsolana-economy/references/after-launch.md +114 -0
- package/skills/xsolana-economy/references/apps.md +157 -0
- package/skills/xsolana-economy/references/sentence-to-spec.md +172 -0
- package/skills/xsolana-economy/references/telling-the-person.md +93 -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 +50 -0
- package/types/chain/curve.d.ts +1 -0
- package/types/chain/lookup.d.ts +191 -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 +66 -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 +178 -0
- package/types/client/compressed.d.ts +29 -0
- package/types/client/grant.d.ts +125 -0
- package/types/client/idl/loot.d.ts +4467 -0
- package/types/client/index.d.ts +8 -0
- package/types/client/ix.d.ts +1687 -0
- package/types/client/pda.d.ts +91 -0
- package/types/client/policy.d.ts +23 -0
- package/types/client/sizes.d.ts +13 -0
- package/types/client/traits.d.ts +12 -0
- package/types/economy/check.d.mts +105 -0
- package/types/economy/cost.d.mts +68 -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 +109 -0
- package/types/economy/model.d.mts +363 -0
- package/types/economy/plan.d.mts +1013 -0
- package/types/economy/secrets.d.mts +4 -0
- package/types/economy/simulate.d.mts +156 -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 +627 -0
- package/types/play/index.d.ts +469 -0
- package/types/results/index.d.ts +155 -0
- package/types/results/wire.d.ts +122 -0
package/js/chain/tx.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Solana transactions without @solana/web3.js: compile instructions into a legacy or v0 message (v0 can load
|
|
2
|
+
// accounts from address lookup tables), wrap a message with signature slots, read a wire transaction back, and put
|
|
3
|
+
// a signature in its slot. Used by browser apps and Node alike.
|
|
4
|
+
import { addressBytes, encodeBase58 } from './base58.js';
|
|
5
|
+
import { compactU16, concat, equalBytes, readCompactU16 } from './bytes.js';
|
|
6
|
+
|
|
7
|
+
/** A serialized transaction must fit one packet. */
|
|
8
|
+
export const MAX_TRANSACTION_BYTES = 1232;
|
|
9
|
+
export const SIGNATURE_BYTES = 64;
|
|
10
|
+
const V0_PREFIX = 0x80;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Every account once, in first-seen order: the payer, then each instruction's program and its accounts. Signing or
|
|
14
|
+
* writable anywhere means signing or writable. Same order as @solana/web3.js's MessageV0.compile.
|
|
15
|
+
*/
|
|
16
|
+
function mergedAccounts(payer, instructions) {
|
|
17
|
+
const merged = new Map([[payer, { pubkey: payer, isSigner: true, isWritable: true, invoked: false }]]);
|
|
18
|
+
const add = (pubkey, isSigner, isWritable, invoked) => {
|
|
19
|
+
addressBytes(pubkey);
|
|
20
|
+
const seen = merged.get(pubkey);
|
|
21
|
+
if (!seen) merged.set(pubkey, { pubkey, isSigner, isWritable, invoked });
|
|
22
|
+
else {
|
|
23
|
+
seen.isSigner ||= isSigner;
|
|
24
|
+
seen.isWritable ||= isWritable;
|
|
25
|
+
seen.invoked ||= invoked;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
for (const ix of instructions) {
|
|
29
|
+
add(ix.programId, false, false, true);
|
|
30
|
+
for (const key of ix.keys) add(key.pubkey, key.isSigner, key.isWritable, false);
|
|
31
|
+
}
|
|
32
|
+
return [...merged.values()];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The order a message lists accounts in: signers first, writable before read-only; payer at the head. */
|
|
36
|
+
const rank = (m) => (m.isSigner ? 0 : 2) + (m.isWritable ? 0 : 1);
|
|
37
|
+
|
|
38
|
+
function compileInstructions(instructions, index) {
|
|
39
|
+
return instructions.map((ix) =>
|
|
40
|
+
concat(
|
|
41
|
+
Uint8Array.of(index.get(ix.programId)),
|
|
42
|
+
compactU16(ix.keys.length),
|
|
43
|
+
Uint8Array.from(ix.keys.map((k) => index.get(k.pubkey))),
|
|
44
|
+
compactU16(ix.data.length),
|
|
45
|
+
ix.data,
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A message. `version` is 'legacy' (default) or 0. For v0, `lookupTables` is [{ address, addresses: [base58] }]:
|
|
52
|
+
* read-only and writable accounts that are neither signers nor invoked programs load from a table when it holds them.
|
|
53
|
+
*/
|
|
54
|
+
export function compileMessage({ payer, blockhash, instructions, version = 'legacy', lookupTables = [] }) {
|
|
55
|
+
if (!instructions?.length) throw new Error('A transaction needs at least one instruction');
|
|
56
|
+
let accounts = mergedAccounts(payer, instructions);
|
|
57
|
+
const lookups = [];
|
|
58
|
+
if (version === 0) {
|
|
59
|
+
for (const table of lookupTables) {
|
|
60
|
+
const drain = (writable) => {
|
|
61
|
+
const indexes = [];
|
|
62
|
+
const keys = [];
|
|
63
|
+
accounts = accounts.filter((m) => {
|
|
64
|
+
if (m.isSigner || m.invoked || m.isWritable !== writable) return true;
|
|
65
|
+
const at = table.addresses.indexOf(m.pubkey);
|
|
66
|
+
if (at < 0) return true;
|
|
67
|
+
if (at > 255) throw new Error('Lookup table index past 255');
|
|
68
|
+
indexes.push(at);
|
|
69
|
+
keys.push(m.pubkey);
|
|
70
|
+
return false;
|
|
71
|
+
});
|
|
72
|
+
return { indexes, keys };
|
|
73
|
+
};
|
|
74
|
+
const w = drain(true);
|
|
75
|
+
const r = drain(false);
|
|
76
|
+
if (w.indexes.length || r.indexes.length)
|
|
77
|
+
lookups.push({ address: table.address, writable: w.indexes, readonly: r.indexes, writableKeys: w.keys, readonlyKeys: r.keys });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const [head, ...rest] = accounts;
|
|
81
|
+
const statics = [head, ...rest.sort((a, b) => rank(a) - rank(b))];
|
|
82
|
+
const loaded = [...lookups.flatMap((l) => l.writableKeys), ...lookups.flatMap((l) => l.readonlyKeys)];
|
|
83
|
+
const index = new Map([...statics.map((m, i) => [m.pubkey, i]), ...loaded.map((k, i) => [k, statics.length + i])]);
|
|
84
|
+
if (index.size > 256) throw new Error('Too many accounts for one transaction');
|
|
85
|
+
const signers = statics.filter((m) => m.isSigner);
|
|
86
|
+
const header = Uint8Array.of(
|
|
87
|
+
signers.length,
|
|
88
|
+
signers.filter((m) => !m.isWritable).length,
|
|
89
|
+
statics.filter((m) => !m.isSigner && !m.isWritable).length,
|
|
90
|
+
);
|
|
91
|
+
const body = concat(
|
|
92
|
+
header,
|
|
93
|
+
compactU16(statics.length),
|
|
94
|
+
...statics.map((m) => addressBytes(m.pubkey)),
|
|
95
|
+
addressBytes(blockhash),
|
|
96
|
+
compactU16(instructions.length),
|
|
97
|
+
...compileInstructions(instructions, index),
|
|
98
|
+
);
|
|
99
|
+
if (version !== 0) return body;
|
|
100
|
+
const tables = lookups.map((l) =>
|
|
101
|
+
concat(
|
|
102
|
+
addressBytes(l.address),
|
|
103
|
+
compactU16(l.writable.length),
|
|
104
|
+
Uint8Array.from(l.writable),
|
|
105
|
+
compactU16(l.readonly.length),
|
|
106
|
+
Uint8Array.from(l.readonly),
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
return concat(Uint8Array.of(V0_PREFIX), body, compactU16(tables.length), ...tables);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The parts of a message: version, header, static keys, blockhash and compiled instructions. Throws, saying so, on a
|
|
114
|
+
* message cut short or with bytes after its end.
|
|
115
|
+
*/
|
|
116
|
+
export function readMessage(message) {
|
|
117
|
+
let at = 0;
|
|
118
|
+
/** The next `n` bytes, refusing to read past the end. */
|
|
119
|
+
const take = (n, what) => {
|
|
120
|
+
if (at + n > message.length) throw new Error(`Truncated message: ${what} runs past its end`);
|
|
121
|
+
const bytes = message.subarray(at, at + n);
|
|
122
|
+
at += n;
|
|
123
|
+
return bytes;
|
|
124
|
+
};
|
|
125
|
+
const length = (what) => {
|
|
126
|
+
if (at >= message.length) throw new Error(`Truncated message: ${what} runs past its end`);
|
|
127
|
+
const prefix = readCompactU16(message, at);
|
|
128
|
+
at += prefix.size;
|
|
129
|
+
return prefix.value;
|
|
130
|
+
};
|
|
131
|
+
const address = (what) => encodeBase58(take(32, what));
|
|
132
|
+
let version = 'legacy';
|
|
133
|
+
if (message.length && message[0] & V0_PREFIX) {
|
|
134
|
+
version = message[0] & 0x7f;
|
|
135
|
+
if (version !== 0) throw new Error(`Unsupported transaction version ${version}`);
|
|
136
|
+
at = 1;
|
|
137
|
+
}
|
|
138
|
+
const [numRequiredSignatures, numReadonlySigned, numReadonlyUnsigned] = take(3, 'the header');
|
|
139
|
+
const header = { numRequiredSignatures, numReadonlySigned, numReadonlyUnsigned };
|
|
140
|
+
const keyCount = length('the account count');
|
|
141
|
+
const keys = [];
|
|
142
|
+
for (let i = 0; i < keyCount; i++) keys.push(address('an account'));
|
|
143
|
+
const blockhash = address('the blockhash');
|
|
144
|
+
const ixCount = length('the instruction count');
|
|
145
|
+
const instructions = [];
|
|
146
|
+
for (let i = 0; i < ixCount; i++) {
|
|
147
|
+
const [programIndex] = take(1, 'an instruction');
|
|
148
|
+
const accountIndexes = Array.from(take(length('an instruction\'s accounts'), 'an instruction\'s accounts'));
|
|
149
|
+
const data = take(length('an instruction\'s data'), 'an instruction\'s data').slice();
|
|
150
|
+
instructions.push({ programIndex, accountIndexes, data });
|
|
151
|
+
}
|
|
152
|
+
const lookups = [];
|
|
153
|
+
if (version === 0) {
|
|
154
|
+
const n = length('the lookup table count');
|
|
155
|
+
for (let i = 0; i < n; i++) {
|
|
156
|
+
const tableAddress = address('a lookup table');
|
|
157
|
+
const writable = Array.from(take(length('a lookup table\'s writable indexes'), 'a lookup table\'s writable indexes'));
|
|
158
|
+
const readonly = Array.from(take(length('a lookup table\'s read-only indexes'), 'a lookup table\'s read-only indexes'));
|
|
159
|
+
lookups.push({ address: tableAddress, writable, readonly });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (at !== message.length) throw new Error('Trailing bytes after the message');
|
|
163
|
+
return { version, header, keys, blockhash, instructions, lookups };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The addresses that must sign a message, in signature-slot order. */
|
|
167
|
+
export function signersOf(message) {
|
|
168
|
+
const { header, keys } = readMessage(message);
|
|
169
|
+
return keys.slice(0, header.numRequiredSignatures);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The wire form: one signature slot per required signer (empty unless given), then the message. */
|
|
173
|
+
export function wireTransaction(message, signatures = []) {
|
|
174
|
+
const count = readMessage(message).header.numRequiredSignatures;
|
|
175
|
+
const slots = Array.from({ length: count }, (_, i) => signatures[i] ?? new Uint8Array(SIGNATURE_BYTES));
|
|
176
|
+
if (slots.some((s) => s.length !== SIGNATURE_BYTES)) throw new Error('A signature is 64 bytes');
|
|
177
|
+
return concat(compactU16(count), ...slots, message);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** A wire transaction read back: its signatures, its message bytes and its signers. */
|
|
181
|
+
export function readTransaction(wire) {
|
|
182
|
+
const count = readCompactU16(wire, 0);
|
|
183
|
+
let at = count.size;
|
|
184
|
+
const signatures = [];
|
|
185
|
+
for (let i = 0; i < count.value; i++) {
|
|
186
|
+
if (at + SIGNATURE_BYTES > wire.length) throw new Error('Truncated signatures');
|
|
187
|
+
signatures.push(wire.slice(at, at + SIGNATURE_BYTES));
|
|
188
|
+
at += SIGNATURE_BYTES;
|
|
189
|
+
}
|
|
190
|
+
const message = wire.slice(at);
|
|
191
|
+
const signers = signersOf(message);
|
|
192
|
+
if (signers.length !== signatures.length) throw new Error('Signature count does not match the message');
|
|
193
|
+
return { signatures, message, signers };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** A copy of the wire transaction with `signer`'s slot holding `signature`. */
|
|
197
|
+
export function withSignature(wire, signer, signature) {
|
|
198
|
+
const { signatures, message, signers } = readTransaction(wire);
|
|
199
|
+
const at = signers.indexOf(signer);
|
|
200
|
+
if (at < 0) throw new Error(`${signer} does not sign this transaction`);
|
|
201
|
+
signatures[at] = signature;
|
|
202
|
+
return wireTransaction(message, signatures);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** The transaction's id: its first signature, in base58. */
|
|
206
|
+
export const transactionId = (wire) => encodeBase58(readTransaction(wire).signatures[0]);
|
|
207
|
+
|
|
208
|
+
/** Whether a signature slot is still empty. */
|
|
209
|
+
export const isEmptySignature = (signature) => signature.every((b) => b === 0);
|
|
210
|
+
|
|
211
|
+
/** Whether two wire transactions carry the same message. */
|
|
212
|
+
export const sameMessage = (a, b) => equalBytes(readTransaction(a).message, readTransaction(b).message);
|
|
213
|
+
|
|
214
|
+
/** The size this message's transaction takes on the wire. */
|
|
215
|
+
export const transactionSize = (message) => wireTransaction(message).length;
|
|
216
|
+
|
|
217
|
+
/** Whether these instructions fit one transaction from this payer. */
|
|
218
|
+
export function fits(input) {
|
|
219
|
+
try {
|
|
220
|
+
return transactionSize(compileMessage(input)) <= MAX_TRANSACTION_BYTES;
|
|
221
|
+
} catch {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
// Reading the loot program's accounts and receipts. Every account starts with a one-byte tag (1 to 23); the IDL
|
|
2
|
+
// describes most of them, and the ones it doesn't (a receipt, a sheet, a stash, …) are read here by hand. Values come back in the
|
|
3
|
+
// IDL's shapes (chain/borsh.js).
|
|
4
|
+
import { encodeBase58, NO_ADDRESS } from '../chain/base58.js';
|
|
5
|
+
import { decodeAccount, decodeEvents } from '../chain/borsh.js';
|
|
6
|
+
import { compareBytes, fromHex, readUintLE, toHex } from '../chain/bytes.js';
|
|
7
|
+
import { sha256 } from '../chain/sha256.js';
|
|
8
|
+
import { SYSTEM_PROGRAM } from '../chain/pda.js';
|
|
9
|
+
import { LOOT_PROGRAM } from './pda.js';
|
|
10
|
+
import IDL from './idl/loot.js';
|
|
11
|
+
import { MAX_RECEIPT_ROUNDS, RECEIPT_BASE_BYTES, RECEIPT_ROUND_BYTES } from './sizes.js';
|
|
12
|
+
export { MAX_RECEIPT_ROUNDS } from './sizes.js';
|
|
13
|
+
|
|
14
|
+
/** Account tags: each account's first byte, as the program writes it. */
|
|
15
|
+
export const TAGS = Object.freeze({
|
|
16
|
+
Platform: 1,
|
|
17
|
+
Project: 2,
|
|
18
|
+
Item: 3,
|
|
19
|
+
LootBox: 4,
|
|
20
|
+
Order: 5,
|
|
21
|
+
Schedule: 6,
|
|
22
|
+
Lottery: 7,
|
|
23
|
+
Recipe: 8,
|
|
24
|
+
Hold: 9,
|
|
25
|
+
Contest: 10,
|
|
26
|
+
Entry: 11,
|
|
27
|
+
Sheet: 12,
|
|
28
|
+
Traits: 13,
|
|
29
|
+
Grant: 14,
|
|
30
|
+
Receipt: 15,
|
|
31
|
+
LotteryContribution: 16,
|
|
32
|
+
Farm: 17,
|
|
33
|
+
Seat: 18,
|
|
34
|
+
Proven: 19,
|
|
35
|
+
Auction: 20,
|
|
36
|
+
Stash: 21,
|
|
37
|
+
Offer: 22,
|
|
38
|
+
Listing: 23,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const BY_TAG = Object.fromEntries(Object.entries(TAGS).map(([name, tag]) => [tag, name]));
|
|
42
|
+
|
|
43
|
+
/** The kind of a loot account from its first byte, or null. */
|
|
44
|
+
export const accountKind = (data) => (data?.length ? BY_TAG[data[0]] ?? null : null);
|
|
45
|
+
|
|
46
|
+
/** A loot account of the named kind (one the IDL describes). Throws when the data is not one. */
|
|
47
|
+
export const decode = (name, data) => decodeAccount(IDL, name, data);
|
|
48
|
+
|
|
49
|
+
/** Any loot account: { kind, value }, or null for data that is no loot account. */
|
|
50
|
+
export function decodeAny(data) {
|
|
51
|
+
const kind = accountKind(data);
|
|
52
|
+
if (!kind) return null;
|
|
53
|
+
if (kind === 'Receipt') return { kind, value: decodeReceipt(data) };
|
|
54
|
+
if (kind === 'Sheet') return { kind, value: decodeSheet(data) };
|
|
55
|
+
if (kind === 'Proven') return { kind, value: decodeProven(data) };
|
|
56
|
+
if (kind === 'Traits') return { kind, value: decodeTraits(data) };
|
|
57
|
+
if (kind === 'Stash') return { kind, value: decodeStash(data) };
|
|
58
|
+
return { kind, value: decode(kind, data) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A counter (a Receipt): a wallet's on a box, a lottery, a recipe or a grant, a person's or a key's on
|
|
63
|
+
* a grant. { count (lifetime where tracked), window (the window `inWindow` counts), inWindow, rounds (the first 8
|
|
64
|
+
* bytes of sha256 of each round a grant paid it, by voucher or list, in order), claimed(round) }. A round it holds
|
|
65
|
+
* was paid once and can never be paid again.
|
|
66
|
+
*/
|
|
67
|
+
export function decodeReceipt(data) {
|
|
68
|
+
if (!(data instanceof Uint8Array) || data[0] !== TAGS.Receipt || data.length < RECEIPT_BASE_BYTES) throw new Error('Not a Receipt account');
|
|
69
|
+
const n = Number(readUintLE(data, RECEIPT_BASE_BYTES - 4, 4));
|
|
70
|
+
if (n > MAX_RECEIPT_ROUNDS) throw new Error('Receipt history exceeds the supported capacity');
|
|
71
|
+
if (data.length !== RECEIPT_BASE_BYTES + RECEIPT_ROUND_BYTES * n) throw new Error('Receipt length does not match its rounds');
|
|
72
|
+
const rounds = Array.from({ length: n }, (_, i) => data.slice(RECEIPT_BASE_BYTES + RECEIPT_ROUND_BYTES * i, RECEIPT_BASE_BYTES + RECEIPT_ROUND_BYTES * (i + 1)));
|
|
73
|
+
if (rounds.some((round, i) => i > 0 && compareBytes(rounds[i - 1], round) >= 0)) throw new Error('Receipt rounds must be unique and sorted');
|
|
74
|
+
const ids = new Set(rounds.map((r) => toHex(r)));
|
|
75
|
+
const claimed = (round) => ids.has(toHex(sha256(typeof round === 'string' ? fromHex(round) : Uint8Array.from(round)).subarray(0, 8)));
|
|
76
|
+
return {
|
|
77
|
+
count: Number(readUintLE(data, 1, 4)),
|
|
78
|
+
window: Number(readUintLE(data, 5, 4)),
|
|
79
|
+
inWindow: Number(readUintLE(data, 9, 4)),
|
|
80
|
+
rounds,
|
|
81
|
+
claimed,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A Sheet (`["sheet", project, nft]`): what the project wrote about an NFT it has no authority over. Returns { project, nft,
|
|
87
|
+
* attributes: [{ key, value }] }. A Sheet never written reads as none: pass `null` data for no account.
|
|
88
|
+
*/
|
|
89
|
+
export function decodeSheet(data) {
|
|
90
|
+
if (data == null || data.length === 0) return null;
|
|
91
|
+
if (!(data instanceof Uint8Array) || data[0] !== TAGS.Sheet || data.length < 69) throw new Error('Not a Sheet account');
|
|
92
|
+
let at = 65;
|
|
93
|
+
// Every length is checked against the data: a count or a text longer than what is there is a truncated account.
|
|
94
|
+
const text = () => {
|
|
95
|
+
const n = Number(readUintLE(data, at, 4));
|
|
96
|
+
if (at + 4 + n > data.length) throw new Error('Sheet account is truncated');
|
|
97
|
+
const value = new TextDecoder().decode(data.subarray(at + 4, at + 4 + n));
|
|
98
|
+
at += 4 + n;
|
|
99
|
+
return value;
|
|
100
|
+
};
|
|
101
|
+
const count = Number(readUintLE(data, at, 4));
|
|
102
|
+
at += 4;
|
|
103
|
+
// Each attribute takes 8 bytes at least (two empty texts).
|
|
104
|
+
if (count * 8 > data.length - at) throw new Error('Sheet account is truncated');
|
|
105
|
+
const attributes = Array.from({ length: count }, () => ({ key: text(), value: text() }));
|
|
106
|
+
return { project: encodeBase58(data.subarray(1, 33)), nft: encodeBase58(data.subarray(33, 65)), attributes };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A stash (`["stash", project, id]`): { project, id, nfts }, the NFTs the project holds to give away, in no order. */
|
|
110
|
+
export function decodeStash(data) {
|
|
111
|
+
if (data == null || data.length === 0) return null;
|
|
112
|
+
if (!(data instanceof Uint8Array) || data[0] !== TAGS.Stash || data.length < 41) throw new Error('Not a Stash account');
|
|
113
|
+
const count = Number(readUintLE(data, 37, 4));
|
|
114
|
+
if (41 + 32 * count > data.length) throw new Error('Stash account is truncated');
|
|
115
|
+
const nfts = Array.from({ length: count }, (_, i) => encodeBase58(data.subarray(41 + 32 * i, 73 + 32 * i)));
|
|
116
|
+
return { project: encodeBase58(data.subarray(1, 33)), id: Number(readUintLE(data, 33, 4)), nfts };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A compressed NFT's record (`["leaf", asset]`, `prove_leaf`): { asset, tree, owner (null once burned), delegate,
|
|
121
|
+
* collection (or null), flags, nonce, root (hex: the tree's root it holds for), payer }. Routes read it only while the
|
|
122
|
+
* tree's root is still `root`; after that anyone may close it (`closeLeafIx`).
|
|
123
|
+
*/
|
|
124
|
+
export function decodeProven(data) {
|
|
125
|
+
// Its shortest: the tag, five addresses, the root, data hash and creator hash, no asset data hash, the flags and
|
|
126
|
+
// nonce, and its payer.
|
|
127
|
+
if (!(data instanceof Uint8Array) || data[0] !== TAGS.Proven || data.length < 1 + 32 * 8 + 1 + 1 + 8 + 32) throw new Error('Not a Proven account');
|
|
128
|
+
const key = (at) => encodeBase58(data.subarray(at, at + 32));
|
|
129
|
+
const none = (k) => (k === NO_ADDRESS ? null : k);
|
|
130
|
+
const root = 1 + 32 * 5;
|
|
131
|
+
let at = root + 32 * 3; // the five addresses, then the root, data hash and creator hash
|
|
132
|
+
if (data[at] !== 0 && data[at] !== 1) throw new Error('Proven account has an unknown asset data hash flag');
|
|
133
|
+
at += data[at] === 1 ? 33 : 1; // the asset data hash
|
|
134
|
+
// With an asset data hash, the flags, nonce and payer come 32 bytes later: the account must hold them too.
|
|
135
|
+
if (at + 1 + 8 + 32 > data.length) throw new Error('Proven account is truncated');
|
|
136
|
+
const flags = data[at];
|
|
137
|
+
return {
|
|
138
|
+
asset: key(1), tree: key(33), owner: none(key(65)), delegate: none(key(97)), collection: none(key(129)),
|
|
139
|
+
flags, nonce: readUintLE(data, at + 1, 8), root: toHex(data.subarray(root, root + 32)), payer: key(at + 9),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The traits a project vouches for (`["traits", project, key]`): { project, key, root (hex) }. */
|
|
144
|
+
export function decodeTraits(data) {
|
|
145
|
+
if (!(data instanceof Uint8Array) || data[0] !== TAGS.Traits || data.length < 97) throw new Error('Not a Traits account');
|
|
146
|
+
return { project: encodeBase58(data.subarray(1, 33)), key: encodeBase58(data.subarray(33, 65)), root: toHex(data.subarray(65, 97)) };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* A Token Metadata account (`MetadataV1`), read as far as a farm needs: { mint, collection (the one it is verified
|
|
151
|
+
* in, or null), programmable (a pNFT), ruleSet (a pNFT's, or null) }. Null for anything else. Older accounts end
|
|
152
|
+
* early: what is not there reads as none.
|
|
153
|
+
*/
|
|
154
|
+
export function decodeTokenMetadata(data) {
|
|
155
|
+
if (!(data instanceof Uint8Array) || data[0] !== 4) return null;
|
|
156
|
+
let at = 33;
|
|
157
|
+
const key = () => encodeBase58(data.subarray(at, (at += 32)));
|
|
158
|
+
const skip = (n) => (at += n);
|
|
159
|
+
const str = () => skip(4 + Number(readUintLE(data, at, 4)));
|
|
160
|
+
const some = () => at < data.length && data[at++] === 1;
|
|
161
|
+
const mint = key();
|
|
162
|
+
str(), str(), str(), skip(2);
|
|
163
|
+
if (some()) skip(34 * Number(readUintLE(data, at, 4)) + 4);
|
|
164
|
+
skip(2);
|
|
165
|
+
if (some()) skip(1); // edition nonce
|
|
166
|
+
const programmable = some() && [4, 5].includes(data[at++]); // a pNFT, or a pNFT's print
|
|
167
|
+
let collection = null;
|
|
168
|
+
if (some()) {
|
|
169
|
+
const verified = data[at++] === 1;
|
|
170
|
+
const address = key();
|
|
171
|
+
if (verified) collection = address;
|
|
172
|
+
}
|
|
173
|
+
if (some()) skip(17); // uses
|
|
174
|
+
if (some()) skip(9); // collection details
|
|
175
|
+
let ruleSet = null;
|
|
176
|
+
if (some()) {
|
|
177
|
+
skip(1); // ProgrammableConfig::V1
|
|
178
|
+
if (some()) ruleSet = key();
|
|
179
|
+
}
|
|
180
|
+
return { mint, collection, programmable, ruleSet };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** A fetched receipt, or null for a missing/prefunded empty System account. Other owners/layouts are refused. */
|
|
184
|
+
export function readReceiptAccount(account) {
|
|
185
|
+
if (account == null) return null;
|
|
186
|
+
if (account.owner === SYSTEM_PROGRAM && account.data instanceof Uint8Array && account.data.length === 0 && !account.executable) return null;
|
|
187
|
+
if (account.owner !== LOOT_PROGRAM || account.executable) throw new Error('The receipt has an invalid owner.');
|
|
188
|
+
return decodeReceipt(account.data);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** What a counter allows `n` more of at `now` under `limit` ({ total, per_window, window }, 0 is no limit). */
|
|
192
|
+
export function receiptAllows(receipt, limit, now, n = 1) {
|
|
193
|
+
const window = Number(limit.window) ? Math.floor(Number(now) / Number(limit.window)) + 1 : 0;
|
|
194
|
+
const count = (receipt?.count ?? 0) + n;
|
|
195
|
+
const inWindow = (receipt && receipt.window === window ? receipt.inWindow : 0) + n;
|
|
196
|
+
return (!Number(limit.total) || count <= Number(limit.total)) && (!Number(limit.per_window) || inWindow <= Number(limit.per_window));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------- a lottery's purchases
|
|
200
|
+
|
|
201
|
+
/** The contribution among `contributions` ([{ address, value }], decoded LotteryContributions) that holds `ticket`. */
|
|
202
|
+
export const contributionOf = (contributions, ticket) =>
|
|
203
|
+
contributions.find(({ value }) => BigInt(value.first_ticket) <= BigInt(ticket) && BigInt(ticket) < BigInt(value.end)) ?? null;
|
|
204
|
+
|
|
205
|
+
/** Whether a lottery's place wins something: a share of the pot, a roll, or both (as the program counts a place set). */
|
|
206
|
+
export const placeSet = (place) => Number(place.bps) > 0 || place.prize != null;
|
|
207
|
+
|
|
208
|
+
/** The places of a lottery that have a winner this round: as many as are set, but no more than tickets. */
|
|
209
|
+
export function lotteryWinners(lottery) {
|
|
210
|
+
const set = lottery.places.findIndex((p) => !placeSet(p));
|
|
211
|
+
return Math.min(set === -1 ? lottery.places.length : set, Number(lottery.sold));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The ticket that wins `place` of a drawn lottery: even steps around the list from first place's ticket. */
|
|
215
|
+
export function ticketOfPlace(lottery, place) {
|
|
216
|
+
const sold = BigInt(lottery.sold);
|
|
217
|
+
const winners = BigInt(lotteryWinners(lottery) || 1);
|
|
218
|
+
const ahead = BigInt(place) * (sold / winners);
|
|
219
|
+
const toEnd = sold - BigInt(lottery.first_ticket);
|
|
220
|
+
return ahead < toEnd ? BigInt(lottery.first_ticket) + ahead : ahead - toEnd;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** The places of a drawn lottery whose winning ticket is in `first..end` (a contribution's), in order. */
|
|
224
|
+
export function placesWon(lottery, first, end) {
|
|
225
|
+
const won = [];
|
|
226
|
+
for (let place = 0; place < lotteryWinners(lottery); place++) {
|
|
227
|
+
const ticket = ticketOfPlace(lottery, place);
|
|
228
|
+
if (BigInt(first) <= ticket && ticket < BigInt(end)) won.push(place);
|
|
229
|
+
}
|
|
230
|
+
return won;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The places of a drawn round that have a winner and a prize, in order: what it gives before its money moves. */
|
|
234
|
+
export const prizePlaces = (lottery) => [...Array(lotteryWinners(lottery)).keys()].filter((place) => lottery.places[place].prize != null);
|
|
235
|
+
|
|
236
|
+
/** The places a contribution won whose prize is not given yet (`Lottery.given`): its next step gives them. */
|
|
237
|
+
export function lotteryOwed(lottery, contribution) {
|
|
238
|
+
if (lottery.status !== 'Drawn') return [];
|
|
239
|
+
const given = Number(lottery.given ?? 0);
|
|
240
|
+
const prizes = new Set(prizePlaces(lottery));
|
|
241
|
+
return placesWon(lottery, contribution.first_ticket, contribution.end).filter((place) => prizes.has(place) && !(given & (1 << place)));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Whether every place's prize of a drawn round is given, so its money may move (`PrizesFirst`). */
|
|
245
|
+
export const prizesGiven = (lottery) => prizePlaces(lottery).every((place) => Number(lottery.given ?? 0) & (1 << place));
|
|
246
|
+
|
|
247
|
+
/** The lowest place of a drawn round whose prize is not given yet, or null: prizes go in place order (`PlacesInOrder`). */
|
|
248
|
+
export const nextPrizePlace = (lottery) => prizePlaces(lottery).find((place) => !(Number(lottery.given ?? 0) & (1 << place))) ?? null;
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* What a contribution's next `finish_lottery_contribution` does, or null for nothing yet:
|
|
252
|
+
* 'prizes' give the prize of the next place it won, one place a call (a drawn round gives every prize before any money moves);
|
|
253
|
+
* 'refund' its tickets back to its buyer (a cancelled round, or a lottery that returns them);
|
|
254
|
+
* 'finish' its money through the shares into the pot, its tickets burned, or its NFT ticket spent (only once every
|
|
255
|
+
* prize of the round is given);
|
|
256
|
+
* 'pay' its places' shares of the pot, once every contribution of the round is finished.
|
|
257
|
+
*/
|
|
258
|
+
export function lotteryStep(lottery, contribution) {
|
|
259
|
+
// Its prize step comes when the lowest place still owed across the round is one it won; until then it waits.
|
|
260
|
+
// In a lottery that returns its tickets, a winner's ticket goes back first, then its prizes.
|
|
261
|
+
const owed = contribution.done || !lottery.returns ? lotteryOwed(lottery, contribution) : [];
|
|
262
|
+
if (owed.length) return owed[0] === nextPrizePlace(lottery) ? 'prizes' : null;
|
|
263
|
+
if (contribution.done) return Number(lottery.pending) === 0 ? 'pay' : null;
|
|
264
|
+
if (lottery.status === 'Cancelled' || (lottery.status === 'Drawn' && lottery.returns)) return 'refund';
|
|
265
|
+
if (lottery.status === 'Drawn') return prizesGiven(lottery) ? 'finish' : null;
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Who rents a listed NFT at `now` (Unix seconds), from its decoded Listing (or null for none): the renter's wallet while
|
|
271
|
+
* a rental runs, else null (a sale listing, or a rental that is over).
|
|
272
|
+
*/
|
|
273
|
+
export function renterOf(listing, now) {
|
|
274
|
+
if (!listing || !listing.deal?.Rent) return null;
|
|
275
|
+
return BigInt(now) < BigInt(listing.until) && listing.user !== SYSTEM_PROGRAM ? listing.user : null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* How many runs of a schedule are due at `now` (Unix seconds, the chain's clock, never the computer's): each its own
|
|
280
|
+
* `run_schedule`, several in one transaction to catch up; one too many fails the whole transaction. A sealed schedule
|
|
281
|
+
* past its last run is due once whenever its vault holds something (`vaultBalance`), which goes where it always went.
|
|
282
|
+
*/
|
|
283
|
+
export function runsDue(schedule, now, vaultBalance = 0n) {
|
|
284
|
+
const runs = Number(schedule.runs), max = Number(schedule.max_runs);
|
|
285
|
+
if (max > 0 && runs >= max) return schedule.action?.from?.Vest && BigInt(vaultBalance) > 0n ? 1 : 0;
|
|
286
|
+
if (!schedule.active || BigInt(now) < BigInt(schedule.next_at)) return 0;
|
|
287
|
+
const due = Number((BigInt(now) - BigInt(schedule.next_at)) / BigInt(schedule.every)) + 1;
|
|
288
|
+
return max > 0 ? Math.min(due, max - runs) : due;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------- receipts
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The receipts (Anchor events) of a transaction that succeeded: [{ name, data }]. Pass what getTransaction returns
|
|
295
|
+
* (`{ meta: { err, logMessages } }`), or the logs and the transaction's `meta.err`. Success is `meta.err === null` and
|
|
296
|
+
* nothing else: a transaction can fail after every instruction logged success (the runtime's rent check, for one),
|
|
297
|
+
* and its logs still hold every receipt, so a transaction with an error gives none. A backend delivers from a
|
|
298
|
+
* finalized transaction's receipts, once per order or reference.
|
|
299
|
+
*/
|
|
300
|
+
export function receipts(tx, err) {
|
|
301
|
+
if (Array.isArray(tx)) {
|
|
302
|
+
if (err === undefined) throw new Error("loot.receipts(logs, err) needs the transaction's meta.err (null when it succeeded): a failed transaction keeps its logs");
|
|
303
|
+
return err === null ? decodeEvents(IDL, tx) : [];
|
|
304
|
+
}
|
|
305
|
+
const meta = tx?.meta;
|
|
306
|
+
if (!meta || meta.err === undefined) throw new Error('loot.receipts(tx) needs the transaction getTransaction returns, with its meta.err and meta.logMessages');
|
|
307
|
+
return meta.err === null ? decodeEvents(IDL, meta.logMessages ?? []) : [];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** The program's errors by code: { code: { name, msg } }. */
|
|
311
|
+
export const ERRORS = Object.freeze(Object.fromEntries(IDL.errors.map((e) => [e.code, { name: e.name, msg: e.msg }])));
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* The loot error in a failed transaction's logs ("custom program error: 0x…", or Anchor's "Error Code: …" line), or
|
|
315
|
+
* null. Only loot's own: another program the transaction calls (Bubblegum, the VRF program, …) has codes of its own
|
|
316
|
+
* that may be the same numbers, so an error is loot's only when loot is the program that logged it or failed with it.
|
|
317
|
+
* The first program that fails is where the error came from; the programs that called it fail after it with the same
|
|
318
|
+
* code.
|
|
319
|
+
*/
|
|
320
|
+
export function lootError(logs) {
|
|
321
|
+
const running = [];
|
|
322
|
+
for (const line of logs ?? []) {
|
|
323
|
+
const invoked = /^Program (\w+) invoke \[\d+\]/.exec(line);
|
|
324
|
+
if (invoked) {
|
|
325
|
+
running.push(invoked[1]);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const failed = /^Program (\w+) failed: (.*)$/.exec(line);
|
|
329
|
+
if (failed) {
|
|
330
|
+
if (failed[1] !== LOOT_PROGRAM) return null;
|
|
331
|
+
const m = /custom program error: 0x([0-9a-f]+)/i.exec(failed[2]);
|
|
332
|
+
const code = m ? parseInt(m[1], 16) : null;
|
|
333
|
+
return code !== null && ERRORS[code] ? { code, ...ERRORS[code] } : null;
|
|
334
|
+
}
|
|
335
|
+
if (/^Program (\w+) success/.test(line)) {
|
|
336
|
+
running.pop();
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
// A line that names no program (a log line, or an error's own text) is the running program's, when it is known.
|
|
340
|
+
if (running.length && running.at(-1) !== LOOT_PROGRAM) continue;
|
|
341
|
+
const named = /Error Code: (\w+)\. Error Number: (\d+)\. Error Message: (.+?)\.?$/.exec(line);
|
|
342
|
+
if (named) return { code: Number(named[2]), name: named[1], msg: named[3] };
|
|
343
|
+
const m = /custom program error: 0x([0-9a-f]+)/i.exec(line);
|
|
344
|
+
if (m && ERRORS[parseInt(m[1], 16)]) return { code: parseInt(m[1], 16), ...ERRORS[parseInt(m[1], 16)] };
|
|
345
|
+
}
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Compressed NFTs (Bubblegum v2) as loot takes them: a leaf as an indexer gives it, read into the shape `proveLeafIx`
|
|
2
|
+
// and the routes take. The chain keeps only a tree's root, changelog and canopy; the leaf and its proof come from an
|
|
3
|
+
// indexer (DAS `getAsset` and `getAssetProof`). Browser-safe.
|
|
4
|
+
import { decodeBase58 } from '../chain/base58.js';
|
|
5
|
+
|
|
6
|
+
/** mpl-account-compression's tree header: type, version, max buffer size (u32), max depth (u32), authority, slot, padding. */
|
|
7
|
+
const HEADER = 56;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* How many of a proof's nodes a transaction passes for this tree: the levels its canopy does not keep. `treeData` is
|
|
11
|
+
* the tree account's bytes.
|
|
12
|
+
*/
|
|
13
|
+
export function proofLength(treeData) {
|
|
14
|
+
const u32 = (at) => treeData[at] | (treeData[at + 1] << 8) | (treeData[at + 2] << 16) | (treeData[at + 3] * 2 ** 24);
|
|
15
|
+
const [buffer, depth] = [u32(2), u32(6)];
|
|
16
|
+
const canopyAt = HEADER + 24 + buffer * (32 * depth + 40) + 32 * depth + 40;
|
|
17
|
+
const canopy = Math.max(0, treeData.length - canopyAt);
|
|
18
|
+
const cached = Math.log2(canopy / 32 + 2) - 1;
|
|
19
|
+
return Math.max(0, depth - Math.round(cached));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const bytes = (value) => (value == null ? null : typeof value === 'string' ? decodeBase58(value) : Uint8Array.from(value));
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A compressed NFT from an indexer's answers: `asset` (DAS `getAsset`), `proof` (DAS `getAssetProof`) and the tree
|
|
26
|
+
* account's bytes (`treeData`, for how much of the proof to pass). Returns { id, tree, nonce, owner, delegate,
|
|
27
|
+
* collection (or null), dataHash, creatorHash, assetDataHash (or null), flags, root, proof: [node, …] }. The proof
|
|
28
|
+
* must be against the tree's current root when it lands: fetch it just before sending.
|
|
29
|
+
*/
|
|
30
|
+
export function compressedFromDas({ asset, proof, treeData }) {
|
|
31
|
+
const c = asset.compression;
|
|
32
|
+
return {
|
|
33
|
+
id: asset.id,
|
|
34
|
+
tree: c.tree,
|
|
35
|
+
nonce: BigInt(c.leaf_id),
|
|
36
|
+
owner: asset.ownership.owner,
|
|
37
|
+
delegate: asset.ownership.delegate ?? asset.ownership.owner,
|
|
38
|
+
collection: asset.grouping?.find((g) => g.group_key === 'collection')?.group_value ?? null,
|
|
39
|
+
dataHash: bytes(c.data_hash),
|
|
40
|
+
creatorHash: bytes(c.creator_hash),
|
|
41
|
+
assetDataHash: bytes(c.asset_data_hash ?? null),
|
|
42
|
+
flags: Number(c.flags ?? 0),
|
|
43
|
+
root: bytes(proof.root),
|
|
44
|
+
proof: proof.proof.slice(0, proofLength(treeData)),
|
|
45
|
+
};
|
|
46
|
+
}
|