@anywalletkit/wallet-near 0.3.0 → 0.4.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/dist/index.cjs CHANGED
@@ -45,6 +45,7 @@ __export(src_exports, {
45
45
  WalletAccountReadOnlyNear: () => WalletAccountReadOnlyNear,
46
46
  WalletManagerNear: () => WalletManagerNear,
47
47
  YOCTO: () => YOCTO,
48
+ accessKeyPermissionTag: () => accessKeyPermissionTag,
48
49
  actionKey: () => actionKey,
49
50
  argsBytes: () => argsBytes,
50
51
  assertWellFormed: () => assertWellFormed,
@@ -361,17 +362,34 @@ function argsBytes(args) {
361
362
  if (!isPlainObject(args)) throw new TypeError("function-call args must be bytes, a string or a plain object");
362
363
  return utf8.encode(JSON.stringify(args));
363
364
  }
365
+ var FUNCTION_CALL_PERMISSION_KEYS = /* @__PURE__ */ new Set(["allowance", "receiverId", "methodNames"]);
366
+ function accessKeyPermissionTag(permission) {
367
+ const keys = isPlainObject(permission) ? Object.keys(permission) : [];
368
+ if (keys.length !== 1) throw new TypeError(`an access-key permission must have exactly one variant, fullAccess or functionCall (has: ${keys.join(", ") || "none"})`);
369
+ const variant = permission[keys[0]];
370
+ if (keys[0] === "fullAccess") {
371
+ if (!isPlainObject(variant)) throw new TypeError("fullAccess must be a plain object");
372
+ return 1;
373
+ }
374
+ if (keys[0] === "functionCall") {
375
+ if (!isPlainObject(variant) || Object.keys(variant).some((k) => !FUNCTION_CALL_PERMISSION_KEYS.has(k))) throw new TypeError("functionCall must be a plain { receiverId, methodNames, allowance? } object");
376
+ if (typeof variant.receiverId !== "string") throw new TypeError("functionCall.receiverId must be a string");
377
+ if (!Array.isArray(variant.methodNames) || !variant.methodNames.every((m) => typeof m === "string")) throw new TypeError("functionCall.methodNames must be an array of strings");
378
+ if (variant.allowance !== void 0 && typeof variant.allowance !== "bigint") throw new TypeError("functionCall.allowance must be a bigint or absent");
379
+ return 0;
380
+ }
381
+ throw new TypeError(`unknown access-key permission ${JSON.stringify(keys[0])}`);
382
+ }
364
383
  function writeAccessKey(w, k) {
365
384
  w.u64(k.nonce);
366
- if ("functionCall" in k.permission) {
385
+ const tag = accessKeyPermissionTag(k.permission);
386
+ w.u8(tag);
387
+ if (tag === 0) {
367
388
  const p = k.permission.functionCall;
368
- w.u8(0);
369
389
  w.option(p.allowance, (a) => w.u128(a));
370
390
  w.string(p.receiverId);
371
391
  w.u32(p.methodNames.length);
372
392
  for (const m of p.methodNames) w.string(m);
373
- } else {
374
- w.u8(1);
375
393
  }
376
394
  }
377
395
  function readAccessKey(r) {
@@ -613,6 +631,13 @@ function httpTransport(url, headers, timeoutMs) {
613
631
  };
614
632
  }
615
633
  var isDefinitive = (e) => e instanceof NearRpcError && !/TIMEOUT|INTERNAL|UNAVAILABLE|NO_SYNCED/i.test(e.errorName ?? "");
634
+ var isDefinitiveBroadcast = (e) => {
635
+ if (!(e instanceof NearRpcError)) return false;
636
+ const err = e.cause;
637
+ const name = typeof err?.name === "string" ? err.name : void 0;
638
+ const causeName = typeof err?.cause?.name === "string" ? err.cause.name : void 0;
639
+ return causeName === "INVALID_TRANSACTION" || causeName === "PARSE_ERROR" || name === "REQUEST_VALIDATION_ERROR";
640
+ };
616
641
  var BROADCAST_METHODS = /* @__PURE__ */ new Set(["send_tx", "broadcast_tx_commit", "broadcast_tx_async"]);
617
642
  function createClient(config) {
618
643
  if (config.provider === void 0) return void 0;
@@ -699,7 +724,7 @@ var NearRpc = class {
699
724
  }
700
725
  };
701
726
  function failureOf(status) {
702
- if (!status || !("Failure" in status)) return void 0;
727
+ if (!status || typeof status !== "object" || !("Failure" in status)) return void 0;
703
728
  const f = status.Failure;
704
729
  try {
705
730
  const json = JSON.stringify(f);
@@ -767,8 +792,15 @@ function describeTransaction(tx, ctx = {}) {
767
792
  if (DANGEROUS_ACTIONS.has(key)) {
768
793
  dangerous ??= KIND_OF_DANGEROUS[key];
769
794
  warnings.push({ code: "dangerous-action", message: `${key} changes the account, its keys or its code.` });
770
- if (key === "addKey" && "fullAccess" in a.addKey.accessKey.permission) {
771
- warnings.push({ code: "full-access-key", message: "Adds a FULL ACCESS key: whoever holds it owns the account." });
795
+ if (key === "addKey") {
796
+ let tag;
797
+ try {
798
+ tag = accessKeyPermissionTag(a.addKey?.accessKey?.permission);
799
+ } catch {
800
+ tag = void 0;
801
+ }
802
+ if (tag === 1) warnings.push({ code: "full-access-key", message: "Adds a FULL ACCESS key: whoever holds it owns the account." });
803
+ else if (tag === void 0) warnings.push({ code: "dangerous-action", message: "Malformed access-key permission: neither fullAccess nor functionCall. The wallet refuses to sign it." });
772
804
  }
773
805
  continue;
774
806
  }
@@ -1116,8 +1148,7 @@ var WalletAccountReadOnlyNear = class _WalletAccountReadOnlyNear {
1116
1148
  /** Status of one of this account's transactions (the `tx` RPC needs the sender). */
1117
1149
  async getTransactionReceipt(hash) {
1118
1150
  const o = await this.requireRpc("fetch transaction receipts").txStatus(hash, this.address);
1119
- if (!o) return void 0;
1120
- return receiptOf(hash, o);
1151
+ return o ? receiptOf(hash, o) : void 0;
1121
1152
  }
1122
1153
  /** Verify an `ed25519:<base58>` (or bare base58 / hex) signature produced by `sign`. */
1123
1154
  async verify(message, signature) {
@@ -1138,8 +1169,10 @@ var WalletAccountReadOnlyNear = class _WalletAccountReadOnlyNear {
1138
1169
  };
1139
1170
  function receiptOf(hash, o) {
1140
1171
  const failure = outcomeFailed(o);
1141
- const pending = o.final_execution_status !== void 0 && /NONE|INCLUDED$/.test(o.final_execution_status) && !failureOf(o.status) && o.status === void 0;
1142
- return { hash, status: failure ? 0 : 1, ...failure ? { failure } : {}, raw: { ...o, ...pending ? { pending: true } : {} } };
1172
+ if (failure) return { hash, status: 0, failure, raw: { ...o } };
1173
+ if (o.final_execution_status !== void 0 && /^(?:NONE|INCLUDED|INCLUDED_FINAL)$/.test(o.final_execution_status)) return void 0;
1174
+ const executed = typeof o.status === "object" && o.status !== null && ("SuccessValue" in o.status || "SuccessReceiptId" in o.status);
1175
+ return executed ? { hash, status: 1, raw: { ...o } } : void 0;
1143
1176
  }
1144
1177
  function parseSignature(signature) {
1145
1178
  const s = signature.startsWith("ed25519:") ? signature.slice("ed25519:".length) : signature;
@@ -1166,6 +1199,15 @@ function assertShape(actions, depth = 0) {
1166
1199
  if (!(f.args instanceof Uint8Array) && typeof f.args !== "string" && !isPlainObject(f.args)) throw new UnsafeTransactionError("malformed-action", "Function-call args must be bytes, a string or a plain object.");
1167
1200
  if (f.gas !== void 0 && typeof f.gas !== "bigint" || f.deposit !== void 0 && typeof f.deposit !== "bigint") throw new UnsafeTransactionError("malformed-action", "Function-call gas and deposit must be bigints.");
1168
1201
  }
1202
+ if ("addKey" in a) {
1203
+ const k = a.addKey;
1204
+ if (!isPlainObject(k) || !isPlainObject(k.accessKey)) throw new UnsafeTransactionError("malformed-action", "addKey must carry an accessKey object.");
1205
+ try {
1206
+ accessKeyPermissionTag(k.accessKey.permission);
1207
+ } catch (e) {
1208
+ throw new UnsafeTransactionError("malformed-action", `AddKey: ${e.message}.`);
1209
+ }
1210
+ }
1169
1211
  if ("signedDelegate" in a && depth === 0) assertShape(a.signedDelegate.delegateAction.actions, depth + 1);
1170
1212
  }
1171
1213
  }
@@ -1238,6 +1280,15 @@ var WalletAccountNear = class _WalletAccountNear extends WalletAccountReadOnlyNe
1238
1280
  signerKind;
1239
1281
  _disposed = false;
1240
1282
  _readOnly;
1283
+ /**
1284
+ * Highest nonce this account has broadcast (accepted, or maybe sent). The access key is read at
1285
+ * `final`, which lags the block that holds our last transaction, so a second send right after
1286
+ * the first would sign the same nonce and be refused. A filled-in nonce is never below it + 1;
1287
+ * NEAR only needs nonces to increase, so a gap left by a transaction that never landed is harmless.
1288
+ */
1289
+ _nonceFloor;
1290
+ /** Tail of this account's send queue: prepare → sign → broadcast runs one send at a time. */
1291
+ _sendTail = Promise.resolve();
1241
1292
  constructor(signer, publicKey, path, config = {}, accountId) {
1242
1293
  super(accountId ?? implicitAccountId(publicKey), config, ed25519Key(publicKey));
1243
1294
  this.#signer = (0, import_core4.requireCurve)(signer, "ed25519");
@@ -1282,15 +1333,44 @@ var WalletAccountNear = class _WalletAccountNear extends WalletAccountReadOnlyNe
1282
1333
  const sig = await this.#signBytes(nep413Hash(payload), "near-nep413");
1283
1334
  return { signature: toBase64(sig), publicKey: publicKeyToString(this.publicKey), accountId: this.address };
1284
1335
  }
1285
- /** Sign a fully built transaction. No checks — see `signTransaction`. */
1336
+ /**
1337
+ * Sign a fully built transaction. It gets the same hard checks as `signTransaction` (this
1338
+ * account's signer and key, no dangerous action unless `allowDangerousActions`, gas limit) but
1339
+ * no node round-trip: no preflight and no fee check.
1340
+ */
1286
1341
  async signBuilt(tx) {
1342
+ const own = this.#ownCopy(tx);
1343
+ this.assertSafe(own);
1344
+ return this.#signChecked(own);
1345
+ }
1346
+ /**
1347
+ * A plain copy of `tx`, decoded from its borsh bytes: the object the checks read is the one that
1348
+ * gets hashed and signed, whatever the caller does with its own object (a getter, or an array
1349
+ * changed while a node call is awaited).
1350
+ */
1351
+ #ownCopy(tx) {
1352
+ let copy;
1353
+ try {
1354
+ copy = decodeTransaction(encodeTransaction(tx));
1355
+ } catch (e) {
1356
+ this.assertSafe(tx);
1357
+ throw new UnsafeTransactionError("non-canonical", `Not an encodable NEAR transaction: ${e.message}`);
1358
+ }
1359
+ return copy;
1360
+ }
1361
+ /** Sign a transaction that has already been through `assertSafe`. */
1362
+ async #signChecked(tx) {
1287
1363
  assertWellFormed(tx);
1288
1364
  const sig = await this.#signBytes(WalletAccountReadOnlyNear.hashOf(tx), "near-tx");
1289
1365
  return { transaction: tx, signature: { keyType: "ed25519", data: sig } };
1290
1366
  }
1291
1367
  /** Build → hard checks → preflight → fee cap. Shared by sign / send / transfer. */
1292
1368
  async prepare(tx, maxFee, op) {
1293
- const built = await this.buildTransaction(tx);
1369
+ let built = await this.buildTransaction(tx);
1370
+ if (typeof tx !== "string" && tx.nonce === void 0 && this._nonceFloor !== void 0 && built.nonce <= this._nonceFloor) {
1371
+ built = { ...built, nonce: this._nonceFloor + 1n };
1372
+ }
1373
+ built = this.#ownCopy(built);
1294
1374
  this.assertSafe(built);
1295
1375
  const estimate = await this.estimate(built);
1296
1376
  this.assertSafe(built, estimate.fee);
@@ -1306,18 +1386,24 @@ var WalletAccountNear = class _WalletAccountNear extends WalletAccountReadOnlyNe
1306
1386
  this.key();
1307
1387
  this.requireRpc("sign transactions");
1308
1388
  const { built } = await this.prepare(tx, (0, import_core4.feeLimit)(this._config.transactionMaxFee, options), "transaction");
1309
- return toBase64(encodeSignedTransaction(await this.signBuilt(built)));
1389
+ return toBase64(encodeSignedTransaction(await this.#signChecked(built)));
1310
1390
  }
1311
1391
  async broadcast(signed) {
1312
1392
  const hash = WalletAccountReadOnlyNear.txHash(signed.transaction);
1313
1393
  const rpc = this.requireRpc("broadcast");
1394
+ const used = () => {
1395
+ const n = signed.transaction.nonce;
1396
+ if (this._nonceFloor === void 0 || n > this._nonceFloor) this._nonceFloor = n;
1397
+ };
1314
1398
  let outcome;
1315
1399
  try {
1316
1400
  outcome = await rpc.sendTx(toBase64(encodeSignedTransaction(signed)));
1317
1401
  } catch (e) {
1318
- if (isDefinitive(e)) throw e;
1402
+ if (isDefinitiveBroadcast(e)) throw e;
1403
+ used();
1319
1404
  throw new BroadcastUnknownError(hash, e);
1320
1405
  }
1406
+ used();
1321
1407
  const failure = outcomeFailed(outcome);
1322
1408
  if (failure) throw new Error(`Transaction ${hash} failed on chain: ${failure}`);
1323
1409
  return outcome.transaction?.hash ?? hash;
@@ -1326,20 +1412,35 @@ var WalletAccountNear = class _WalletAccountNear extends WalletAccountReadOnlyNe
1326
1412
  * Sign and broadcast. Accepts a transaction, or base64 borsh of an unsigned /
1327
1413
  * already-signed transaction (a signed one is checked, not re-signed).
1328
1414
  */
1329
- async sendTransaction(tx, options = {}) {
1415
+ sendTransaction(tx, options = {}) {
1416
+ return this.#serial(() => this.#sendTransactionNow(tx, options));
1417
+ }
1418
+ async #sendTransactionNow(tx, options) {
1330
1419
  this.key();
1331
1420
  this.requireRpc("send transactions");
1332
1421
  const presigned = typeof tx === "string" ? decodeSigned(tx) : void 0;
1333
1422
  const { built, estimate } = await this.prepare(tx, (0, import_core4.feeLimit)(this._config.transactionMaxFee, options), "transaction");
1334
- const signed = presigned ?? await this.signBuilt(built);
1423
+ const signed = presigned ?? await this.#signChecked(built);
1335
1424
  return { hash: await this.broadcast(signed), fee: estimate.fee, estimate };
1336
1425
  }
1337
1426
  /** Native NEAR (no `token`) or NEP-141 `ft_transfer` (with `storage_deposit` when the recipient needs it). */
1338
- async transfer(o, writeOptions = {}) {
1427
+ transfer(o, writeOptions = {}) {
1428
+ return this.#serial(() => this.#transferNow(o, writeOptions));
1429
+ }
1430
+ async #transferNow(o, writeOptions) {
1339
1431
  this.key();
1340
1432
  this.requireRpc("transfer tokens");
1341
1433
  const { built, estimate } = await this.prepare(await this.transferActions(o), (0, import_core4.feeLimit)(this._config.transferMaxFee, writeOptions), "transfer");
1342
- return { hash: await this.broadcast(await this.signBuilt(built)), fee: estimate.fee, estimate };
1434
+ return { hash: await this.broadcast(await this.#signChecked(built)), fee: estimate.fee, estimate };
1435
+ }
1436
+ /**
1437
+ * One send at a time per account: two sends started together would read the same access-key
1438
+ * nonce before either is broadcast. A failed send does not block the next.
1439
+ */
1440
+ #serial(run) {
1441
+ const next = this._sendTail.then(run);
1442
+ this._sendTail = next.then(() => void 0, () => void 0);
1443
+ return next;
1343
1444
  }
1344
1445
  /** The same account as a {@link WalletAccountReadOnlyNear}. */
1345
1446
  toReadOnlyAccount() {