@jamscript/client 0.1.0-rc.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/LICENSE +201 -0
- package/README.md +126 -0
- package/dist/abi.d.ts +101 -0
- package/dist/abi.js +18 -0
- package/dist/client.d.ts +69 -0
- package/dist/client.js +339 -0
- package/dist/codec.d.ts +8 -0
- package/dist/codec.js +314 -0
- package/dist/crypto.d.ts +65 -0
- package/dist/crypto.js +233 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/matrix.d.ts +23 -0
- package/dist/matrix.js +31 -0
- package/dist/proof.d.ts +1 -0
- package/dist/proof.js +125 -0
- package/dist/rpc.d.ts +115 -0
- package/dist/rpc.js +83 -0
- package/dist/runtime.d.ts +53 -0
- package/dist/runtime.js +405 -0
- package/dist/signer.d.ts +27 -0
- package/dist/signer.js +33 -0
- package/dist/state-provider.d.ts +51 -0
- package/dist/state-provider.js +173 -0
- package/package.json +55 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { actionByName, queryByName, stateByName } from "./abi.js";
|
|
2
|
+
import { decodeStateValue, decodeValue, encodeActionPayload, encodeValue } from "./codec.js";
|
|
3
|
+
import { actionSelector, encodeSignedActionV1, encodeSignedActionV2, MANAGED_STATE_COMMITMENT_KEY_V1, nonceKey, ownershipNonceKey, parseHex, signingDigestV1, signingMessageV2, stateKey, toHex, } from "./crypto.js";
|
|
4
|
+
import { asWorkRpc, RpcError } from "./rpc.js";
|
|
5
|
+
import { blake2AsU8a } from "@polkadot/util-crypto";
|
|
6
|
+
import { verifyManagedStateProof } from "./proof.js";
|
|
7
|
+
import { ProofStateProvider, TrustedStateProvider, } from "./state-provider.js";
|
|
8
|
+
const EMPTY_STATE_ROOT_V1 = "0x03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314";
|
|
9
|
+
export class JamScriptClient {
|
|
10
|
+
deployment;
|
|
11
|
+
options;
|
|
12
|
+
rpc;
|
|
13
|
+
actionHashes = new Map();
|
|
14
|
+
nextNonces = new Map();
|
|
15
|
+
nonceTails = new Map();
|
|
16
|
+
ownershipTails = new Map();
|
|
17
|
+
constructor(deployment, transport, options = {}) {
|
|
18
|
+
this.deployment = deployment;
|
|
19
|
+
this.options = options;
|
|
20
|
+
if (deployment.abiVersion !== 1 || deployment.abi.abiVersion !== 1) {
|
|
21
|
+
throw new Error("unsupported JamScript ABI version");
|
|
22
|
+
}
|
|
23
|
+
this.rpc = asWorkRpc(transport);
|
|
24
|
+
this.stateProvider = options.stateProvider
|
|
25
|
+
?? (options.stateVerification === "proof"
|
|
26
|
+
? new ProofStateProvider(transport)
|
|
27
|
+
: new TrustedStateProvider(transport));
|
|
28
|
+
this.verifyProofs = options.stateProvider !== undefined || options.stateVerification === "proof";
|
|
29
|
+
}
|
|
30
|
+
stateProvider;
|
|
31
|
+
verifyProofs;
|
|
32
|
+
async validateDeployment() {
|
|
33
|
+
const genesis = await this.rpc.genesisHash();
|
|
34
|
+
if (!sameHex(genesis, this.deployment.genesisHash)) {
|
|
35
|
+
throw new Error("deployment genesis hash does not match the chain");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async readNonce(publicKey, context) {
|
|
39
|
+
if (publicKey.length !== 32)
|
|
40
|
+
throw new Error("sr25519 public key must be 32 bytes");
|
|
41
|
+
const finalized = context ?? (await this.rpc.finalizedContext());
|
|
42
|
+
const root = await this.managedStateRoot(finalized);
|
|
43
|
+
const key = nonceKey(publicKey);
|
|
44
|
+
const valueBytes = await this.readManagedValue(root, key);
|
|
45
|
+
if (valueBytes === null)
|
|
46
|
+
return 0n;
|
|
47
|
+
const value = decodeValue("u64", valueBytes);
|
|
48
|
+
if (typeof value !== "bigint")
|
|
49
|
+
throw new Error("nonce storage is not u64");
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
async submitAction(actionName, input, signer, options = {}) {
|
|
53
|
+
if (signer.publicKey.length !== 32)
|
|
54
|
+
throw new Error("sr25519 public key must be 32 bytes");
|
|
55
|
+
const signerKey = toHex(signer.publicKey).toLowerCase();
|
|
56
|
+
const prepared = await this.prepareAction(actionName, input, signer, options);
|
|
57
|
+
let submitted;
|
|
58
|
+
try {
|
|
59
|
+
submitted = await this.rpc.submitTransaction(prepared.request);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
// Only rewind a reservation when no later concurrent action has already
|
|
63
|
+
// reserved a nonce. This keeps the common single-admission-failure
|
|
64
|
+
// recovery while avoiding duplicate nonces for an active batch.
|
|
65
|
+
if (this.nextNonces.get(signerKey) === prepared.nonce + 1n) {
|
|
66
|
+
this.nextNonces.delete(signerKey);
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
this.actionHashes.set(submitted.transactionId.toLowerCase(), prepared.actionHash);
|
|
71
|
+
return { ...submitted, actionHash: prepared.actionHash };
|
|
72
|
+
}
|
|
73
|
+
async submitOwnershipAction(actionName, input, signer, options = {}) {
|
|
74
|
+
const controller = await signer.getController();
|
|
75
|
+
const signerLane = toHex(controller.public).toLowerCase();
|
|
76
|
+
const previous = this.ownershipTails.get(signerLane) ?? Promise.resolve();
|
|
77
|
+
let release;
|
|
78
|
+
const lane = new Promise((resolve) => { release = resolve; });
|
|
79
|
+
this.ownershipTails.set(signerLane, lane);
|
|
80
|
+
await previous;
|
|
81
|
+
try {
|
|
82
|
+
await this.validateDeployment();
|
|
83
|
+
const action = actionByName(this.deployment.abi, actionName);
|
|
84
|
+
if (!isOwnershipAuth(action.auth))
|
|
85
|
+
throw new Error("submitOwnershipAction requires an ownership-authenticated action");
|
|
86
|
+
const payload = encodeActionPayload(this.deployment.abi, actionName, input);
|
|
87
|
+
const selector = actionSelector(actionName);
|
|
88
|
+
if (!sameHex(toHex(selector), action.selector))
|
|
89
|
+
throw new Error("deployment ABI selector does not match the canonical selector");
|
|
90
|
+
const context = await this.rpc.finalizedContext();
|
|
91
|
+
const effectiveOwner = options.actAs ?? controller;
|
|
92
|
+
const root = await this.managedStateRoot(context);
|
|
93
|
+
const nonceBytes = await this.readManagedValue(root, ownershipNonceKey(effectiveOwner));
|
|
94
|
+
const chainNonce = nonceBytes === null ? 0n : decodeValue("u64", nonceBytes);
|
|
95
|
+
if (typeof chainNonce !== "bigint")
|
|
96
|
+
throw new Error("ownership nonce storage is not u64");
|
|
97
|
+
const unsigned = {
|
|
98
|
+
version: 2,
|
|
99
|
+
networkDomain: parseHex(this.deployment.networkDomain, 32),
|
|
100
|
+
serviceKey: parseHex(this.deployment.serviceKey, 32),
|
|
101
|
+
actionSelector: selector,
|
|
102
|
+
controller,
|
|
103
|
+
actAs: options.actAs ?? null,
|
|
104
|
+
nonce: chainNonce,
|
|
105
|
+
validUntil: BigInt(context.slot) + (options.ttl ?? 64n),
|
|
106
|
+
payloadHash: blake2(payload),
|
|
107
|
+
payload,
|
|
108
|
+
};
|
|
109
|
+
const message = signingMessageV2(unsigned);
|
|
110
|
+
const authorizationProof = await signer.signJamScriptAction({ ...unsigned, message });
|
|
111
|
+
if (authorizationProof.length === 0 || authorizationProof.length > 65536)
|
|
112
|
+
throw new Error("invalid Ownership authorization proof");
|
|
113
|
+
const signed = encodeSignedActionV2({ ...unsigned, authorizationProof });
|
|
114
|
+
const actionHash = toHex(blake2(signed));
|
|
115
|
+
const submitted = await this.rpc.submitTransaction({
|
|
116
|
+
serviceId: this.deployment.serviceId,
|
|
117
|
+
serviceCodeHash: this.deployment.codeHash,
|
|
118
|
+
payloadBase64: toBase64(signed),
|
|
119
|
+
extrinsicsBase64: (options.extrinsics ?? []).map(toBase64),
|
|
120
|
+
});
|
|
121
|
+
this.actionHashes.set(submitted.transactionId.toLowerCase(), actionHash);
|
|
122
|
+
return { ...submitted, actionHash };
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
release();
|
|
126
|
+
if (this.ownershipTails.get(signerLane) === lane)
|
|
127
|
+
this.ownershipTails.delete(signerLane);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async reserveWalletNonce(publicKey) {
|
|
131
|
+
const signerKey = toHex(publicKey).toLowerCase();
|
|
132
|
+
const previous = this.nonceTails.get(signerKey) ?? Promise.resolve();
|
|
133
|
+
let release;
|
|
134
|
+
const lane = new Promise((resolve) => { release = resolve; });
|
|
135
|
+
this.nonceTails.set(signerKey, lane);
|
|
136
|
+
await previous;
|
|
137
|
+
try {
|
|
138
|
+
const context = await this.rpc.finalizedContext();
|
|
139
|
+
const localNonce = this.nextNonces.get(signerKey);
|
|
140
|
+
const chainNonce = localNonce === undefined
|
|
141
|
+
? await this.readNonce(publicKey, context)
|
|
142
|
+
: localNonce;
|
|
143
|
+
const nonce = localNonce === undefined || localNonce < chainNonce ? chainNonce : localNonce;
|
|
144
|
+
this.nextNonces.set(signerKey, nonce + 1n);
|
|
145
|
+
return { context, nonce };
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
release();
|
|
149
|
+
if (this.nonceTails.get(signerKey) === lane)
|
|
150
|
+
this.nonceTails.delete(signerKey);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async prepareAction(actionName, input, signer, options) {
|
|
154
|
+
await this.validateDeployment();
|
|
155
|
+
const action = actionByName(this.deployment.abi, actionName);
|
|
156
|
+
if (action.auth !== "wallet")
|
|
157
|
+
throw new Error("submitAction requires a wallet-authenticated action");
|
|
158
|
+
if (signer.publicKey.length !== 32)
|
|
159
|
+
throw new Error("sr25519 public key must be 32 bytes");
|
|
160
|
+
const payload = encodeActionPayload(this.deployment.abi, actionName, input);
|
|
161
|
+
const selector = actionSelector(actionName);
|
|
162
|
+
if (!sameHex(toHex(selector), action.selector)) {
|
|
163
|
+
throw new Error("deployment ABI selector does not match the canonical selector");
|
|
164
|
+
}
|
|
165
|
+
const { context: initialContext, nonce } = await this.reserveWalletNonce(signer.publicKey);
|
|
166
|
+
const ttl = options.ttl ?? 64n;
|
|
167
|
+
const validUntil = BigInt(initialContext.slot) + ttl;
|
|
168
|
+
const unsigned = {
|
|
169
|
+
version: 1,
|
|
170
|
+
networkDomain: parseHex(this.deployment.networkDomain, 32),
|
|
171
|
+
serviceKey: parseHex(this.deployment.serviceKey, 32),
|
|
172
|
+
actionSelector: selector,
|
|
173
|
+
signerScheme: 0,
|
|
174
|
+
publicKey: signer.publicKey,
|
|
175
|
+
nonce,
|
|
176
|
+
validUntil,
|
|
177
|
+
payloadHash: blake2(payload),
|
|
178
|
+
payload,
|
|
179
|
+
};
|
|
180
|
+
const signature = await signer.signRaw(signingDigestV1(unsigned));
|
|
181
|
+
if (signature.length !== 64)
|
|
182
|
+
throw new Error("sr25519 signRaw must return a 64-byte signature");
|
|
183
|
+
const signed = encodeSignedActionV1({ ...unsigned, signature });
|
|
184
|
+
const actionHash = toHex(blake2(signed));
|
|
185
|
+
const requestBase = {
|
|
186
|
+
serviceId: this.deployment.serviceId,
|
|
187
|
+
serviceCodeHash: this.deployment.codeHash,
|
|
188
|
+
payloadBase64: toBase64(signed),
|
|
189
|
+
extrinsicsBase64: (options.extrinsics ?? []).map(toBase64),
|
|
190
|
+
};
|
|
191
|
+
return { request: requestBase, actionHash, nonce };
|
|
192
|
+
}
|
|
193
|
+
async queryLatest(queryName, key) {
|
|
194
|
+
const query = queryByName(this.deployment.abi, queryName);
|
|
195
|
+
const state = stateByName(this.deployment.abi, query.state);
|
|
196
|
+
const keyBytes = isUnitType(state.keyType)
|
|
197
|
+
? new Uint8Array()
|
|
198
|
+
: encodeValue(state.keyType, key === undefined ? null : key);
|
|
199
|
+
if (JSON.stringify(query.keyType) !== JSON.stringify(state.keyType))
|
|
200
|
+
throw new Error("query key type does not match state key type");
|
|
201
|
+
const context = await this.rpc.finalizedContext();
|
|
202
|
+
const root = await this.managedStateRoot(context);
|
|
203
|
+
const valueBytes = await this.readManagedValue(root, stateKey(state.schema, keyBytes));
|
|
204
|
+
return {
|
|
205
|
+
value: valueBytes === null
|
|
206
|
+
? null
|
|
207
|
+
: decodeValue(query.output.type, valueBytes),
|
|
208
|
+
context,
|
|
209
|
+
stateRoot: toHex(root),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
async query(queryName, key) {
|
|
213
|
+
return this.queryLatest(queryName, key);
|
|
214
|
+
}
|
|
215
|
+
async managedStateRoot(context) {
|
|
216
|
+
const encoded = await this.rpc.serviceStorageAt(context.blockHash, this.deployment.serviceId, toHex(MANAGED_STATE_COMMITMENT_KEY_V1));
|
|
217
|
+
if (encoded === null)
|
|
218
|
+
return parseHex(EMPTY_STATE_ROOT_V1);
|
|
219
|
+
const commitment = decodeStateValue(parseHex(encoded));
|
|
220
|
+
if (commitment.length !== 34 || commitment[0] !== 1 || commitment[1] !== 1) {
|
|
221
|
+
throw new Error("invalid ManagedStateCommitmentV1");
|
|
222
|
+
}
|
|
223
|
+
return commitment.slice(2);
|
|
224
|
+
}
|
|
225
|
+
async readManagedValue(root, key) {
|
|
226
|
+
const response = await this.stateProvider.get({
|
|
227
|
+
serviceId: this.deployment.serviceId,
|
|
228
|
+
serviceKey: this.deployment.serviceKey,
|
|
229
|
+
stateRoot: toHex(root),
|
|
230
|
+
key,
|
|
231
|
+
});
|
|
232
|
+
if (response.serviceId !== this.deployment.serviceId
|
|
233
|
+
|| response.stateRoot.toLowerCase() !== toHex(root).toLowerCase()
|
|
234
|
+
|| !sameBytes(response.key, key)) {
|
|
235
|
+
throw new Error("managed-state provider response does not match the requested query");
|
|
236
|
+
}
|
|
237
|
+
if (this.verifyProofs)
|
|
238
|
+
return verifyManagedStateProof(root, key, response.value, response.proof);
|
|
239
|
+
return response.value;
|
|
240
|
+
}
|
|
241
|
+
workStatus(packageHash) {
|
|
242
|
+
return this.rpc.workStatus(packageHash, this.deployment.serviceId);
|
|
243
|
+
}
|
|
244
|
+
transactionStatus(transactionId) {
|
|
245
|
+
return this.rpc.transactionStatus(transactionId);
|
|
246
|
+
}
|
|
247
|
+
async waitForTransaction(transactionId, options = {}) {
|
|
248
|
+
const intervalMs = options.intervalMs ?? 1_000;
|
|
249
|
+
const deadline = Date.now() + (options.timeoutMs ?? 120_000);
|
|
250
|
+
for (;;) {
|
|
251
|
+
try {
|
|
252
|
+
const status = await this.transactionStatus(transactionId);
|
|
253
|
+
if (status.status === "imported" || status.status === "failed")
|
|
254
|
+
return status;
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (!(error instanceof RpcError) || error.code !== -32013)
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
if (Date.now() >= deadline)
|
|
261
|
+
throw new Error("timed out waiting for transaction");
|
|
262
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async waitForWork(packageHash, options = {}) {
|
|
266
|
+
const intervalMs = options.intervalMs ?? 1_000;
|
|
267
|
+
const deadline = Date.now() + (options.timeoutMs ?? 120_000);
|
|
268
|
+
for (;;) {
|
|
269
|
+
try {
|
|
270
|
+
const status = await this.workStatus(packageHash);
|
|
271
|
+
if (status.status === "imported" || status.status === "failed")
|
|
272
|
+
return status;
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
if (!(error instanceof RpcError)
|
|
276
|
+
|| (error.code !== -32013 && error.message !== "work not found"))
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
if (Date.now() >= deadline)
|
|
280
|
+
throw new Error("timed out waiting for finalized Work");
|
|
281
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async waitForAction(transactionId, optionsOrActionHash = {}, legacyOptions = {}) {
|
|
285
|
+
const options = typeof optionsOrActionHash === "string" ? legacyOptions : optionsOrActionHash;
|
|
286
|
+
const transaction = await this.waitForTransaction(transactionId, options);
|
|
287
|
+
if (transaction.status === "failed") {
|
|
288
|
+
throw new RpcError("transaction failed before an action receipt was produced", -32040, transaction);
|
|
289
|
+
}
|
|
290
|
+
const expected = this.actionHashes.get(transactionId.toLowerCase())
|
|
291
|
+
?? (typeof optionsOrActionHash === "string" ? optionsOrActionHash : undefined);
|
|
292
|
+
if (!expected)
|
|
293
|
+
throw new Error("action hash is unavailable for this client instance");
|
|
294
|
+
const actionIndex = transaction.actionIndex;
|
|
295
|
+
const receipt = actionIndex === null ? undefined : transaction.actionReceipts?.[actionIndex];
|
|
296
|
+
if (!receipt) {
|
|
297
|
+
throw new Error("canonical action receipt is missing for the transaction action index");
|
|
298
|
+
}
|
|
299
|
+
if (!sameHex(receipt.actionHash, expected)) {
|
|
300
|
+
throw new Error("transaction action index resolved to a different action hash");
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
...transaction,
|
|
304
|
+
status: receipt.status,
|
|
305
|
+
transactionStatus: transaction.status,
|
|
306
|
+
actionHash: receipt.actionHash,
|
|
307
|
+
errorCode: receipt.errorCode,
|
|
308
|
+
actionReceipt: receipt,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function isUnitType(type) {
|
|
313
|
+
return typeof type === "string" ? type === "unit" : type.kind === "unit";
|
|
314
|
+
}
|
|
315
|
+
function isOwnershipAuth(auth) {
|
|
316
|
+
return typeof auth === "object" && auth.kind === "ownership" && auth.version === 1;
|
|
317
|
+
}
|
|
318
|
+
function blake2(bytes) {
|
|
319
|
+
return blake2AsU8a(bytes, 256);
|
|
320
|
+
}
|
|
321
|
+
function toBase64(bytes) {
|
|
322
|
+
let binary = "";
|
|
323
|
+
for (const byte of bytes)
|
|
324
|
+
binary += String.fromCharCode(byte);
|
|
325
|
+
return btoa(binary);
|
|
326
|
+
}
|
|
327
|
+
function fromBase64(value) {
|
|
328
|
+
const binary = atob(value);
|
|
329
|
+
const output = new Uint8Array(binary.length);
|
|
330
|
+
for (let index = 0; index < binary.length; index += 1)
|
|
331
|
+
output[index] = binary.charCodeAt(index);
|
|
332
|
+
return output;
|
|
333
|
+
}
|
|
334
|
+
function sameBytes(left, right) {
|
|
335
|
+
return left.length === right.length && left.every((byte, index) => byte === right[index]);
|
|
336
|
+
}
|
|
337
|
+
function sameHex(left, right) {
|
|
338
|
+
return left.toLowerCase().replace(/^0x/, "") === right.toLowerCase().replace(/^0x/, "");
|
|
339
|
+
}
|
package/dist/codec.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AbiTypeRef, JamScriptAbi } from "./abi.js";
|
|
2
|
+
export type CodecValue = null | undefined | bigint | number | boolean | string | Uint8Array | CodecValue[] | {
|
|
3
|
+
[key: string]: CodecValue;
|
|
4
|
+
};
|
|
5
|
+
export declare function encodeValue(type: AbiTypeRef, value: CodecValue): Uint8Array;
|
|
6
|
+
export declare function decodeValue(type: AbiTypeRef, bytes: Uint8Array): CodecValue;
|
|
7
|
+
export declare function encodeActionPayload(abi: JamScriptAbi, actionName: string, values: Record<string, CodecValue>): Uint8Array;
|
|
8
|
+
export declare function decodeStateValue(encoded: Uint8Array): Uint8Array;
|
package/dist/codec.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { decodeOwnership, encodeOwnership } from "./crypto.js";
|
|
2
|
+
class Writer {
|
|
3
|
+
chunks = [];
|
|
4
|
+
push(bytes) { this.chunks.push(bytes); }
|
|
5
|
+
finish() { const size = this.chunks.reduce((sum, chunk) => sum + chunk.length, 0); const output = new Uint8Array(size); let offset = 0; for (const chunk of this.chunks) {
|
|
6
|
+
output.set(chunk, offset);
|
|
7
|
+
offset += chunk.length;
|
|
8
|
+
} return output; }
|
|
9
|
+
}
|
|
10
|
+
class Reader {
|
|
11
|
+
bytes;
|
|
12
|
+
offset = 0;
|
|
13
|
+
constructor(bytes) {
|
|
14
|
+
this.bytes = bytes;
|
|
15
|
+
}
|
|
16
|
+
take(length) { const end = this.offset + length; if (!Number.isSafeInteger(end) || end > this.bytes.length)
|
|
17
|
+
throw new Error("truncated JAM value"); const result = this.bytes.slice(this.offset, end); this.offset = end; return result; }
|
|
18
|
+
u8() { return this.take(1)[0]; }
|
|
19
|
+
natural() { const first = this.u8(); if (first < 0x80)
|
|
20
|
+
return BigInt(first); const extra = first === 0xff ? 8 : Math.clz32((~first) & 0xff) - 24; if (extra === 8)
|
|
21
|
+
return new DataView(this.take(8).buffer).getBigUint64(0, true); let result = 0n; for (let i = 0; i < extra; i += 1)
|
|
22
|
+
result |= BigInt(this.u8()) << BigInt(i * 8); return result | BigInt(first & (0x7f >> extra)) << BigInt(extra * 8); }
|
|
23
|
+
remaining() { return this.bytes.length - this.offset; }
|
|
24
|
+
}
|
|
25
|
+
function compact(value) { if (value < 0n)
|
|
26
|
+
throw new Error("length cannot be negative"); if (value < 128n)
|
|
27
|
+
return Uint8Array.of(Number(value)); if (value < (1n << 56n)) {
|
|
28
|
+
let extra = 0;
|
|
29
|
+
let threshold = 128n;
|
|
30
|
+
while (value >= threshold) {
|
|
31
|
+
extra += 1;
|
|
32
|
+
threshold <<= 7n;
|
|
33
|
+
}
|
|
34
|
+
const output = new Uint8Array(extra + 1);
|
|
35
|
+
output[0] = (256 - (1 << (8 - extra))) | Number(value >> BigInt(extra * 8));
|
|
36
|
+
let left = value;
|
|
37
|
+
for (let i = 0; i < extra; i += 1) {
|
|
38
|
+
output[i + 1] = Number(left & 0xffn);
|
|
39
|
+
left >>= 8n;
|
|
40
|
+
}
|
|
41
|
+
return output;
|
|
42
|
+
} const output = new Uint8Array(9); output[0] = 0xff; let left = value; for (let i = 0; i < 8; i += 1) {
|
|
43
|
+
output[i + 1] = Number(left & 0xffn);
|
|
44
|
+
left >>= 8n;
|
|
45
|
+
} if (left !== 0n)
|
|
46
|
+
throw new Error("natural is out of range"); return output; }
|
|
47
|
+
function asBytes(value) { if (!(value instanceof Uint8Array))
|
|
48
|
+
throw new Error("expected Uint8Array"); return value; }
|
|
49
|
+
function integer(value, min, max, type) { const result = typeof value === "bigint" ? value : typeof value === "number" && Number.isSafeInteger(value) ? BigInt(value) : null; if (result === null)
|
|
50
|
+
throw new Error(type + " must be a bigint or safe integer"); if (result < min || result > max)
|
|
51
|
+
throw new Error(type + " is out of range"); return result; }
|
|
52
|
+
function le(value, width) { const result = new Uint8Array(width); let left = BigInt.asUintN(width * 8, value); for (let i = 0; i < width; i += 1) {
|
|
53
|
+
result[i] = Number(left & 0xffn);
|
|
54
|
+
left >>= 8n;
|
|
55
|
+
} return result; }
|
|
56
|
+
function readLe(reader, width, signed) { const data = reader.take(width); let result = 0n; for (let i = width - 1; i >= 0; i -= 1)
|
|
57
|
+
result = (result << 8n) | BigInt(data[i]); return signed ? BigInt.asIntN(width * 8, result) : result; }
|
|
58
|
+
function descriptor(type) {
|
|
59
|
+
if (typeof type !== "string")
|
|
60
|
+
return type;
|
|
61
|
+
const bounded = /^(Bytes|bytes|String|string)<([0-9]+)>$/.exec(type);
|
|
62
|
+
if (bounded)
|
|
63
|
+
return { kind: bounded[1].toLowerCase() === "bytes" ? "bytes" : "string", max: Number(bounded[2]) };
|
|
64
|
+
const fixed = /^(FixedBytes|fixedBytes)<([0-9]+)>$/.exec(type);
|
|
65
|
+
if (fixed)
|
|
66
|
+
return { kind: "fixedBytes", len: Number(fixed[2]) };
|
|
67
|
+
if (["unit", "bool", "u8", "u16", "u32", "u64", "u128", "i8", "i16", "i32", "i64", "i128", "address", "ownership"].includes(type))
|
|
68
|
+
return { kind: type };
|
|
69
|
+
throw new Error("unsupported ABI type: " + type);
|
|
70
|
+
}
|
|
71
|
+
function encode(type, value, writer) {
|
|
72
|
+
const ty = descriptor(type);
|
|
73
|
+
switch (ty.kind) {
|
|
74
|
+
case "unit": return;
|
|
75
|
+
case "bool":
|
|
76
|
+
if (typeof value !== "boolean")
|
|
77
|
+
throw new Error("bool must be a boolean");
|
|
78
|
+
writer.push(Uint8Array.of(value ? 1 : 0));
|
|
79
|
+
return;
|
|
80
|
+
case "u8":
|
|
81
|
+
writer.push(Uint8Array.of(Number(integer(value, 0n, 0xffn, "u8"))));
|
|
82
|
+
return;
|
|
83
|
+
case "u16":
|
|
84
|
+
writer.push(le(integer(value, 0n, 0xffffn, "u16"), 2));
|
|
85
|
+
return;
|
|
86
|
+
case "u32":
|
|
87
|
+
writer.push(le(integer(value, 0n, 0xffffffffn, "u32"), 4));
|
|
88
|
+
return;
|
|
89
|
+
case "u64":
|
|
90
|
+
writer.push(le(integer(value, 0n, 0xffffffffffffffffn, "u64"), 8));
|
|
91
|
+
return;
|
|
92
|
+
case "u128":
|
|
93
|
+
writer.push(le(integer(value, 0n, (1n << 128n) - 1n, "u128"), 16));
|
|
94
|
+
return;
|
|
95
|
+
case "i8":
|
|
96
|
+
writer.push(le(integer(value, -128n, 127n, "i8"), 1));
|
|
97
|
+
return;
|
|
98
|
+
case "i16":
|
|
99
|
+
writer.push(le(integer(value, -32768n, 32767n, "i16"), 2));
|
|
100
|
+
return;
|
|
101
|
+
case "i32":
|
|
102
|
+
writer.push(le(integer(value, -2147483648n, 2147483647n, "i32"), 4));
|
|
103
|
+
return;
|
|
104
|
+
case "i64":
|
|
105
|
+
writer.push(le(integer(value, -(1n << 63n), (1n << 63n) - 1n, "i64"), 8));
|
|
106
|
+
return;
|
|
107
|
+
case "i128":
|
|
108
|
+
writer.push(le(integer(value, -(1n << 127n), (1n << 127n) - 1n, "i128"), 16));
|
|
109
|
+
return;
|
|
110
|
+
case "address": {
|
|
111
|
+
const data = asBytes(value);
|
|
112
|
+
if (data.length !== 32)
|
|
113
|
+
throw new Error("address must be 32 bytes");
|
|
114
|
+
writer.push(data);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
case "ownership": {
|
|
118
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || value instanceof Uint8Array)
|
|
119
|
+
throw new Error("ownership must be an Ownership object");
|
|
120
|
+
writer.push(encodeOwnership(value));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
case "fixedBytes": {
|
|
124
|
+
const data = asBytes(value);
|
|
125
|
+
if (data.length !== ty.len)
|
|
126
|
+
throw new Error(`fixedBytes length must be ${ty.len}`);
|
|
127
|
+
writer.push(data);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
case "bytes": {
|
|
131
|
+
const data = asBytes(value);
|
|
132
|
+
if (data.length > ty.max)
|
|
133
|
+
throw new Error("bytes value exceeds its bound");
|
|
134
|
+
writer.push(compact(BigInt(data.length)));
|
|
135
|
+
writer.push(data);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
case "string": {
|
|
139
|
+
if (typeof value !== "string")
|
|
140
|
+
throw new Error("string must be a string");
|
|
141
|
+
const data = new TextEncoder().encode(value);
|
|
142
|
+
if (data.length > ty.max)
|
|
143
|
+
throw new Error("string value exceeds its UTF-8 byte bound");
|
|
144
|
+
writer.push(compact(BigInt(data.length)));
|
|
145
|
+
writer.push(data);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
case "fixedArray":
|
|
149
|
+
if (!Array.isArray(value) || value.length !== ty.len)
|
|
150
|
+
throw new Error("fixedArray length mismatch");
|
|
151
|
+
value.forEach(item => encode(ty.item, item, writer));
|
|
152
|
+
return;
|
|
153
|
+
case "array":
|
|
154
|
+
if (!Array.isArray(value) || value.length > ty.max)
|
|
155
|
+
throw new Error("array value exceeds its bound");
|
|
156
|
+
writer.push(compact(BigInt(value.length)));
|
|
157
|
+
value.forEach(item => encode(ty.item, item, writer));
|
|
158
|
+
return;
|
|
159
|
+
case "option":
|
|
160
|
+
if (value === null || value === undefined)
|
|
161
|
+
writer.push(Uint8Array.of(0));
|
|
162
|
+
else {
|
|
163
|
+
writer.push(Uint8Array.of(1));
|
|
164
|
+
encode(ty.item, value, writer);
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
case "tuple":
|
|
168
|
+
if (!Array.isArray(value) || value.length !== ty.items.length)
|
|
169
|
+
throw new Error("tuple length mismatch");
|
|
170
|
+
ty.items.forEach((item, i) => encode(item, value[i], writer));
|
|
171
|
+
return;
|
|
172
|
+
case "record":
|
|
173
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || value instanceof Uint8Array)
|
|
174
|
+
throw new Error("record must be an object");
|
|
175
|
+
ty.fields.forEach(field => { if (!(field.name in value))
|
|
176
|
+
throw new Error("missing record field: " + field.name); encode(field.type, value[field.name], writer); });
|
|
177
|
+
return;
|
|
178
|
+
case "enum": {
|
|
179
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || value instanceof Uint8Array)
|
|
180
|
+
throw new Error("enum must contain index and value");
|
|
181
|
+
const enumeration = value;
|
|
182
|
+
if (typeof enumeration.index !== "number" || enumeration.value === undefined)
|
|
183
|
+
throw new Error("enum must contain index and value");
|
|
184
|
+
const variant = ty.variants.find(candidate => candidate.index === enumeration.index);
|
|
185
|
+
if (!variant)
|
|
186
|
+
throw new Error("invalid enum variant");
|
|
187
|
+
writer.push(Uint8Array.of(enumeration.index));
|
|
188
|
+
encode(variant.type, enumeration.value, writer);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
case "result": {
|
|
192
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || value instanceof Uint8Array)
|
|
193
|
+
throw new Error("result must be an object");
|
|
194
|
+
const result = value;
|
|
195
|
+
const ok = "ok" in result;
|
|
196
|
+
writer.push(Uint8Array.of(ok ? 0 : 1));
|
|
197
|
+
encode(ok ? ty.ok : ty.err, ok ? result.ok : result.err, writer);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
default: throw new Error("unsupported ABI type");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function decode(reader, type) {
|
|
204
|
+
const ty = descriptor(type);
|
|
205
|
+
switch (ty.kind) {
|
|
206
|
+
case "unit": return undefined;
|
|
207
|
+
case "bool": {
|
|
208
|
+
const value = reader.u8();
|
|
209
|
+
if (value > 1)
|
|
210
|
+
throw new Error("invalid bool value");
|
|
211
|
+
return value === 1;
|
|
212
|
+
}
|
|
213
|
+
case "u8": return Number(readLe(reader, 1, false));
|
|
214
|
+
case "u16": return Number(readLe(reader, 2, false));
|
|
215
|
+
case "u32": return Number(readLe(reader, 4, false));
|
|
216
|
+
case "u64": return readLe(reader, 8, false);
|
|
217
|
+
case "u128": return readLe(reader, 16, false);
|
|
218
|
+
case "i8": return Number(readLe(reader, 1, true));
|
|
219
|
+
case "i16": return Number(readLe(reader, 2, true));
|
|
220
|
+
case "i32": return Number(readLe(reader, 4, true));
|
|
221
|
+
case "i64": return readLe(reader, 8, true);
|
|
222
|
+
case "i128": return readLe(reader, 16, true);
|
|
223
|
+
case "address": return reader.take(32);
|
|
224
|
+
case "ownership": {
|
|
225
|
+
const start = reader.take(4);
|
|
226
|
+
const length = start[2] | (start[3] << 8);
|
|
227
|
+
return decodeOwnership(new Uint8Array([...start, ...reader.take(length)]));
|
|
228
|
+
}
|
|
229
|
+
case "fixedBytes": return reader.take(ty.len);
|
|
230
|
+
case "bytes": {
|
|
231
|
+
const length = reader.natural();
|
|
232
|
+
if (length > BigInt(ty.max))
|
|
233
|
+
throw new Error("bytes value exceeds its bound");
|
|
234
|
+
return reader.take(Number(length));
|
|
235
|
+
}
|
|
236
|
+
case "string": {
|
|
237
|
+
const length = reader.natural();
|
|
238
|
+
if (length > BigInt(ty.max))
|
|
239
|
+
throw new Error("string value exceeds its UTF-8 byte bound");
|
|
240
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(reader.take(Number(length)));
|
|
241
|
+
}
|
|
242
|
+
case "fixedArray": return Array.from({ length: ty.len }, () => decode(reader, ty.item));
|
|
243
|
+
case "array": {
|
|
244
|
+
const length = reader.natural();
|
|
245
|
+
if (length > BigInt(ty.max))
|
|
246
|
+
throw new Error("array value exceeds its bound");
|
|
247
|
+
return Array.from({ length: Number(length) }, () => decode(reader, ty.item));
|
|
248
|
+
}
|
|
249
|
+
case "option": {
|
|
250
|
+
const tag = reader.u8();
|
|
251
|
+
if (tag === 0)
|
|
252
|
+
return null;
|
|
253
|
+
if (tag !== 1)
|
|
254
|
+
throw new Error("invalid option tag");
|
|
255
|
+
return decode(reader, ty.item);
|
|
256
|
+
}
|
|
257
|
+
case "tuple": return ty.items.map(item => decode(reader, item));
|
|
258
|
+
case "record": return Object.fromEntries(ty.fields.map(field => [field.name, decode(reader, field.type)]));
|
|
259
|
+
case "enum": {
|
|
260
|
+
const index = reader.u8();
|
|
261
|
+
const variant = ty.variants.find(candidate => candidate.index === index);
|
|
262
|
+
if (!variant)
|
|
263
|
+
throw new Error("invalid enum variant");
|
|
264
|
+
return { index, value: decode(reader, variant.type) };
|
|
265
|
+
}
|
|
266
|
+
case "result": {
|
|
267
|
+
const tag = reader.u8();
|
|
268
|
+
if (tag === 0)
|
|
269
|
+
return { ok: decode(reader, ty.ok) };
|
|
270
|
+
if (tag === 1)
|
|
271
|
+
return { err: decode(reader, ty.err) };
|
|
272
|
+
throw new Error("invalid result tag");
|
|
273
|
+
}
|
|
274
|
+
default: throw new Error("unsupported ABI type");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
export function encodeValue(type, value) { const writer = new Writer(); encode(type, value, writer); return writer.finish(); }
|
|
278
|
+
export function decodeValue(type, bytes) { const reader = new Reader(bytes); const result = decode(reader, type); if (reader.remaining() !== 0)
|
|
279
|
+
throw new Error("trailing bytes in ABI value"); return result; }
|
|
280
|
+
export function encodeActionPayload(abi, actionName, values) { const action = abi.actions.find(candidate => candidate.name === actionName); if (!action)
|
|
281
|
+
throw new Error("unknown JamScript action: " + actionName); const writer = new Writer(); for (const field of action.input) {
|
|
282
|
+
if (!(field.name in values))
|
|
283
|
+
throw new Error("missing action field: " + field.name);
|
|
284
|
+
encode(field.type, values[field.name], writer);
|
|
285
|
+
} return writer.finish(); }
|
|
286
|
+
export function decodeStateValue(encoded) {
|
|
287
|
+
if (encoded.length === 0)
|
|
288
|
+
throw new Error("empty StateValue");
|
|
289
|
+
const first = encoded[0];
|
|
290
|
+
let length;
|
|
291
|
+
let offset;
|
|
292
|
+
switch (first & 3) {
|
|
293
|
+
case 0:
|
|
294
|
+
length = first >>> 2;
|
|
295
|
+
offset = 1;
|
|
296
|
+
break;
|
|
297
|
+
case 1:
|
|
298
|
+
if (encoded.length < 2)
|
|
299
|
+
throw new Error("truncated StateValue length");
|
|
300
|
+
length = ((encoded[0] | (encoded[1] << 8)) >>> 2);
|
|
301
|
+
offset = 2;
|
|
302
|
+
break;
|
|
303
|
+
case 2:
|
|
304
|
+
if (encoded.length < 4)
|
|
305
|
+
throw new Error("truncated StateValue length");
|
|
306
|
+
length = ((encoded[0] | (encoded[1] << 8) | (encoded[2] << 16) | (encoded[3] << 24)) >>> 2);
|
|
307
|
+
offset = 4;
|
|
308
|
+
break;
|
|
309
|
+
default: throw new Error("StateValue is too large for browser client");
|
|
310
|
+
}
|
|
311
|
+
if (offset + length !== encoded.length)
|
|
312
|
+
throw new Error("invalid StateValue length");
|
|
313
|
+
return encoded.slice(offset);
|
|
314
|
+
}
|