@arcanetech/privacy-sdk-stellar 0.2.0 → 0.3.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.js CHANGED
@@ -273,11 +273,14 @@ async function commitStorageChanges(input, eventId, receipt) {
273
273
  });
274
274
  if (input.prepared.outputRecords.length > 0) {
275
275
  await input.storage.savePrivateRecords(
276
- input.prepared.outputRecords.map((record) => ({
277
- ...record,
278
- txHash: receipt.operationId,
279
- status: receipt.confirmed ? "finalized" : record.status
280
- }))
276
+ input.prepared.outputRecords.map((record) => {
277
+ const nextStatus = receipt.confirmed ? "finalized" : record.status;
278
+ return {
279
+ ...record,
280
+ txHash: receipt.operationId,
281
+ ...nextStatus === void 0 ? {} : { status: nextStatus }
282
+ };
283
+ })
281
284
  );
282
285
  }
283
286
  if (input.prepared.consumedRecords.length > 0) {
@@ -333,7 +336,10 @@ import { insufficientStateError as insufficientStateError3 } from "@arcanetech/p
333
336
 
334
337
  // src/transact/transfer-source/resolve.ts
335
338
  import { Buffer as Buffer2 } from "buffer";
336
- import { insufficientStateError, invalidIntentError } from "@arcanetech/privacy-sdk-core";
339
+ import {
340
+ insufficientStateError,
341
+ invalidIntentError
342
+ } from "@arcanetech/privacy-sdk-core";
337
343
  var FIELD_HEX_LENGTH = 64;
338
344
  var SCALAR_HEX_PATTERN = new RegExp(`^(0x)?[0-9a-f]{${FIELD_HEX_LENGTH}}$`, "iu");
339
345
  function hexToDecimal(hex) {
@@ -366,6 +372,7 @@ function pendingClaimToCoin(claim) {
366
372
  }
367
373
  function pendingClaimToConsumedRecord(input) {
368
374
  const coin = pendingClaimToCoin(input.claim);
375
+ const nullifierHex = input.claim.nullifierHex ?? input.claim.futureNullifierHashHex;
369
376
  return {
370
377
  id: `pending-claim-${input.claim.id}`,
371
378
  owner: input.walletPublicKey.trim(),
@@ -374,9 +381,9 @@ function pendingClaimToConsumedRecord(input) {
374
381
  consumed: false,
375
382
  status: "finalized",
376
383
  poolContract: input.poolContract,
377
- commitmentHex: input.claim.commitmentHex,
378
- nullifierHex: input.claim.nullifierHex ?? input.claim.futureNullifierHashHex,
379
- coinNote: coin
384
+ coinNote: coin,
385
+ ...input.claim.commitmentHex ? { commitmentHex: input.claim.commitmentHex } : {},
386
+ ...nullifierHex ? { nullifierHex } : {}
380
387
  };
381
388
  }
382
389
  function readPendingClaimFromSource(source) {
@@ -479,7 +486,7 @@ function createInternalPoolContractClient(input) {
479
486
  networkPassphrase: input.networkPassphrase,
480
487
  walletPublicKey: input.walletPublicKey
481
488
  });
482
- return new PrivacyPoolContractClient({
489
+ const client = new PrivacyPoolContractClient({
483
490
  contractId: input.contractId,
484
491
  networkPassphrase: input.networkPassphrase,
485
492
  rpcUrl: input.sorobanRpcUrl,
@@ -487,6 +494,7 @@ function createInternalPoolContractClient(input) {
487
494
  publicKey: input.walletPublicKey,
488
495
  signTransaction: signBinding
489
496
  });
497
+ return client;
490
498
  }
491
499
 
492
500
  // src/contracts/registry/create-registry-client.ts
@@ -1686,7 +1694,7 @@ function pickRecordsForSpendAmount(input) {
1686
1694
  if (selectedTotal >= input.amount) {
1687
1695
  return selected;
1688
1696
  }
1689
- if (input.kind === "transfer" && selected.length >= 2) {
1697
+ if ((input.kind === "transfer" || input.kind === "withdraw") && selected.length >= 2) {
1690
1698
  throw insufficientStateError3(
1691
1699
  "missing_private_records",
1692
1700
  "Storage does not contain enough private records for this operation.",
@@ -1784,7 +1792,7 @@ async function resolvePendingClaimSpend(intent, deps, walletPublicKey) {
1784
1792
  await assertPendingClaimNullifierAvailable({
1785
1793
  claim,
1786
1794
  walletPublicKey,
1787
- checkNullifierSpent: deps.checkNullifierSpent
1795
+ ...deps.checkNullifierSpent ? { checkNullifierSpent: deps.checkNullifierSpent } : {}
1788
1796
  });
1789
1797
  validatePendingClaimIntentMatches(claim, intent);
1790
1798
  return {
@@ -1810,12 +1818,12 @@ async function resolvePrivateAddressSpend(intent, deps) {
1810
1818
  kind: "transfer",
1811
1819
  intent,
1812
1820
  walletPublicKey,
1813
- checkNullifierSpent: deps.checkNullifierSpent
1821
+ ...deps.checkNullifierSpent ? { checkNullifierSpent: deps.checkNullifierSpent } : {}
1814
1822
  });
1815
1823
  await assertPrivateRecordsNullifiersAvailable({
1816
1824
  records: consumedRecords,
1817
1825
  walletPublicKey: walletAddress.trim(),
1818
- checkNullifierSpent: deps.checkNullifierSpent
1826
+ ...deps.checkNullifierSpent ? { checkNullifierSpent: deps.checkNullifierSpent } : {}
1819
1827
  });
1820
1828
  return {
1821
1829
  consumedRecords,
@@ -1897,7 +1905,7 @@ async function resolvePrepareConsumedRecords(input) {
1897
1905
  storage: input.storage,
1898
1906
  intent: input.intent,
1899
1907
  walletPublicKey: await loadSpendSelectionWalletPublicKey(input.wallet),
1900
- checkNullifierSpent: input.checkNullifierSpent
1908
+ ...input.checkNullifierSpent ? { checkNullifierSpent: input.checkNullifierSpent } : {}
1901
1909
  });
1902
1910
  }
1903
1911
  function buildDepositOrWithdrawPrepared(input) {
@@ -5613,7 +5621,8 @@ async function resolveWalletPublicKey(environment, prepared) {
5613
5621
  return environment.resolveWalletPublicKey();
5614
5622
  }
5615
5623
  if (prepared.kind === "deposit") {
5616
- return prepared.intent.from;
5624
+ const depositIntent = prepared.intent;
5625
+ return depositIntent.from;
5617
5626
  }
5618
5627
  throw new Error("Wallet public key resolver is not configured.");
5619
5628
  }
@@ -6407,11 +6416,21 @@ function enrichWithdrawOutputRecords(prepared, proof) {
6407
6416
  precommitementHex: changeCoin.precommitementHex
6408
6417
  }));
6409
6418
  }
6410
- async function finalizeWithdrawAtExecute(prepared, environment, poolService) {
6419
+ function buildWithdrawFinalizeArtifacts(proof, context) {
6420
+ return {
6421
+ proofHex: proof.proof_hex,
6422
+ publicHex: proof.public_hex,
6423
+ applicationIdsPlaintext: proof.applicationIdsPlaintext,
6424
+ tokenAddress: context.tokenAddress,
6425
+ walletPublicKey: context.walletPublicKey,
6426
+ executeFinalizeRequired: false
6427
+ };
6428
+ }
6429
+ async function finalizeSingleWithdrawAtExecute(prepared, environment, poolService, withdrawFrom) {
6411
6430
  const context = await buildSpendProofContextAtExecute({
6412
6431
  prepared,
6413
6432
  environment,
6414
- recipientPrivateAddressStpl1: prepared.intent.from
6433
+ recipientPrivateAddressStpl1: withdrawFrom
6415
6434
  });
6416
6435
  const changeStroops = BigInt(context.coin.value) - prepared.intent.amount;
6417
6436
  const proof = await poolService.prepareWithdrawTransactProof({
@@ -6424,18 +6443,75 @@ async function finalizeWithdrawAtExecute(prepared, environment, poolService) {
6424
6443
  yHex: context.ephemeral.yHex
6425
6444
  }),
6426
6445
  withdrawAmountStroops: prepared.intent.amount,
6427
- changePrivateAddressStpl1: changeStroops > 0n ? prepared.intent.from : void 0,
6446
+ changePrivateAddressStpl1: changeStroops > 0n ? withdrawFrom : void 0,
6428
6447
  tokenAddress: context.tokenAddress
6429
6448
  });
6430
6449
  enrichWithdrawOutputRecords(prepared, proof);
6431
- return {
6432
- proofHex: proof.proof_hex,
6433
- publicHex: proof.public_hex,
6434
- applicationIdsPlaintext: proof.applicationIdsPlaintext,
6435
- tokenAddress: context.tokenAddress,
6450
+ return buildWithdrawFinalizeArtifacts(proof, context);
6451
+ }
6452
+ async function finalizeDualWithdrawAtExecute(prepared, environment, poolService, withdrawFrom) {
6453
+ const [, secondaryRecord] = prepared.consumedRecords;
6454
+ if (!secondaryRecord) {
6455
+ throw new Error("Dual withdraw execution requires two private input records.");
6456
+ }
6457
+ const context = await buildSpendProofContextAtExecute({
6458
+ prepared,
6459
+ environment,
6460
+ recipientPrivateAddressStpl1: withdrawFrom
6461
+ });
6462
+ const secondary = await readCoinEphemeralForRecord({
6463
+ record: secondaryRecord,
6464
+ environment,
6436
6465
  walletPublicKey: context.walletPublicKey,
6437
- executeFinalizeRequired: false
6438
- };
6466
+ commitments: context.commitments
6467
+ });
6468
+ const totalNotes = BigInt(context.coin.value) + BigInt(secondary.coin.value);
6469
+ const changeStroops = totalNotes - prepared.intent.amount;
6470
+ const proof = await poolService.prepareWithdrawTransactProofDual({
6471
+ coinA: context.coin,
6472
+ coinB: secondary.coin,
6473
+ state: { commitments: context.commitments },
6474
+ destinationStellarAddress: prepared.intent.to,
6475
+ privKeyScalarHex: context.senderPrivKeyScalarHex,
6476
+ depositorEphemeralAKey: serializeEphemeralKeyString({
6477
+ xHex: context.ephemeral.xHex,
6478
+ yHex: context.ephemeral.yHex
6479
+ }),
6480
+ depositorEphemeralBKey: serializeEphemeralKeyString({
6481
+ xHex: secondary.ephemeral.xHex,
6482
+ yHex: secondary.ephemeral.yHex
6483
+ }),
6484
+ withdrawAmountStroops: prepared.intent.amount,
6485
+ changePrivateAddressStpl1: changeStroops > 0n ? withdrawFrom : void 0,
6486
+ tokenAddress: context.tokenAddress
6487
+ });
6488
+ enrichWithdrawOutputRecords(prepared, proof);
6489
+ return buildWithdrawFinalizeArtifacts(proof, context);
6490
+ }
6491
+ async function finalizeWithdrawAtExecute(prepared, environment, poolService) {
6492
+ if (prepared.kind !== "withdraw") {
6493
+ throw new Error("Withdraw finalize requires a withdraw operation.");
6494
+ }
6495
+ const withdrawIntent = prepared.intent;
6496
+ const withdrawFrom = withdrawIntent.from;
6497
+ const recordCount = prepared.consumedRecords.length;
6498
+ if (recordCount === 1) {
6499
+ return finalizeSingleWithdrawAtExecute(
6500
+ prepared,
6501
+ environment,
6502
+ poolService,
6503
+ withdrawFrom
6504
+ );
6505
+ }
6506
+ if (recordCount === 2) {
6507
+ return finalizeDualWithdrawAtExecute(
6508
+ prepared,
6509
+ environment,
6510
+ poolService,
6511
+ withdrawFrom
6512
+ );
6513
+ }
6514
+ throw new Error("Confidential withdraw execution supports at most two input notes.");
6439
6515
  }
6440
6516
  async function finalizeSpendOperationAtExecute(prepared, environment, poolService) {
6441
6517
  if (prepared.kind !== "transfer" && prepared.kind !== "withdraw") {
@@ -6593,6 +6669,12 @@ async function registerKytPassage(parameters) {
6593
6669
  await waitForKytRegistration({ server, hash: result.hash });
6594
6670
  }
6595
6671
 
6672
+ // src/transact/environment/zk-config-nonce.ts
6673
+ var DEFAULT_ZK_CONFIG_NONCE = 0n;
6674
+ function resolveZkConfigNonce(transactEnvironment) {
6675
+ return transactEnvironment.zkConfigNonce ?? DEFAULT_ZK_CONFIG_NONCE;
6676
+ }
6677
+
6596
6678
  // src/transact/kyt/passage/inspect.ts
6597
6679
  function buildApplicationIdHints(applicationId) {
6598
6680
  return [applicationId, applicationId, applicationId, applicationId];
@@ -6646,6 +6728,7 @@ async function requestKytPassageForPoolInteraction(input) {
6646
6728
  decryptedAuditSlots: input.decryptedAuditSlots,
6647
6729
  onboarding: input.onboarding,
6648
6730
  nonce: buildNonce(),
6731
+ zkConfigNonce: resolveZkConfigNonce(input.transactEnvironment),
6649
6732
  currentLedger
6650
6733
  },
6651
6734
  kytJsonReplacer
@@ -6668,11 +6751,123 @@ async function requestKytPassageForPoolInteraction(input) {
6668
6751
  return body;
6669
6752
  }
6670
6753
 
6754
+ // src/transact/submit/zk-config-ttl.ts
6755
+ import {
6756
+ Address as Address2,
6757
+ Operation as Operation2,
6758
+ SorobanDataBuilder,
6759
+ TransactionBuilder as TransactionBuilder2,
6760
+ rpc as rpc4,
6761
+ xdr as xdr3
6762
+ } from "@stellar/stellar-sdk";
6763
+ var ZK_CONFIG_TTL_THRESHOLD_LEDGERS = 1e5;
6764
+ var ZK_CONFIG_TTL_EXTEND_TO_LEDGERS = 5e5;
6765
+ var EXTEND_TTL_FEE_STROOPS = "1000000";
6766
+ var EXTEND_TTL_TIMEOUT_SECONDS = 300;
6767
+ var EXTEND_TTL_MAX_POLLS = 60;
6768
+ var EXTEND_TTL_POLL_INTERVAL_MS = 1e3;
6769
+ async function waitForExtendFootprintTtlConfirmation(parameters) {
6770
+ for (let attempt = 0; attempt < EXTEND_TTL_MAX_POLLS; attempt += 1) {
6771
+ const response = await parameters.server.getTransaction(parameters.hash);
6772
+ if (response.status === "SUCCESS") {
6773
+ return;
6774
+ }
6775
+ if (response.status === "FAILED") {
6776
+ throw new Error("ZK config TTL extension failed on-chain");
6777
+ }
6778
+ await new Promise((resolve) => {
6779
+ setTimeout(resolve, EXTEND_TTL_POLL_INTERVAL_MS);
6780
+ });
6781
+ }
6782
+ throw new Error("ZK config TTL extension was not confirmed in time");
6783
+ }
6784
+ function zkConfigLedgerKey(contractId, nonce) {
6785
+ const configKeyScValue = xdr3.ScVal.scvVec([
6786
+ xdr3.ScVal.scvSymbol("Config"),
6787
+ xdr3.ScVal.scvU64(xdr3.Uint64.fromString(nonce.toString()))
6788
+ ]);
6789
+ return xdr3.LedgerKey.contractData(
6790
+ new xdr3.LedgerKeyContractData({
6791
+ contract: new Address2(contractId).toScAddress(),
6792
+ key: configKeyScValue,
6793
+ durability: xdr3.ContractDataDurability.persistent()
6794
+ })
6795
+ );
6796
+ }
6797
+ async function needsTtlExtension(parameters) {
6798
+ const [entriesResponse, latestLedger] = await Promise.all([
6799
+ parameters.server.getLedgerEntries(parameters.ledgerKey),
6800
+ parameters.server.getLatestLedger()
6801
+ ]);
6802
+ const entry = entriesResponse.entries.at(0);
6803
+ if (!entry || entry.liveUntilLedgerSeq === void 0) {
6804
+ return false;
6805
+ }
6806
+ const remainingLedgers = entry.liveUntilLedgerSeq - latestLedger.sequence;
6807
+ return remainingLedgers < ZK_CONFIG_TTL_THRESHOLD_LEDGERS;
6808
+ }
6809
+ async function submitExtendFootprintTtlTransaction(parameters) {
6810
+ const sourceAccount = await parameters.server.getAccount(parameters.sourcePublicKey);
6811
+ const transaction = new TransactionBuilder2(sourceAccount, {
6812
+ fee: EXTEND_TTL_FEE_STROOPS,
6813
+ networkPassphrase: parameters.networkPassphrase
6814
+ }).addOperation(
6815
+ Operation2.extendFootprintTtl({ extendTo: ZK_CONFIG_TTL_EXTEND_TO_LEDGERS })
6816
+ ).setSorobanData(
6817
+ new SorobanDataBuilder().setReadOnly([parameters.ledgerKey]).build()
6818
+ ).setTimeout(EXTEND_TTL_TIMEOUT_SECONDS).build();
6819
+ const preparedTransaction = await parameters.server.prepareTransaction(transaction);
6820
+ const { signedTxXdr } = await parameters.signTransaction({
6821
+ xdr: preparedTransaction.toXDR(),
6822
+ networkPassphrase: parameters.networkPassphrase,
6823
+ address: parameters.sourcePublicKey
6824
+ });
6825
+ const signedTransaction = TransactionBuilder2.fromXDR(
6826
+ signedTxXdr,
6827
+ parameters.networkPassphrase
6828
+ );
6829
+ const result = await parameters.server.sendTransaction(signedTransaction);
6830
+ const status = String(result.status);
6831
+ if (status !== "PENDING" && status !== "SUCCESS") {
6832
+ throw new Error(`ZK config TTL extension submit failed: ${status}`);
6833
+ }
6834
+ await waitForExtendFootprintTtlConfirmation({
6835
+ server: parameters.server,
6836
+ hash: result.hash
6837
+ });
6838
+ }
6839
+ async function extendZkConfigTtlIfNeeded(parameters) {
6840
+ if (!parameters.signTransaction) {
6841
+ return;
6842
+ }
6843
+ const server = new rpc4.Server(parameters.sorobanRpcUrl, { allowHttp: true });
6844
+ const ledgerKey = zkConfigLedgerKey(parameters.contractId, parameters.nonce);
6845
+ if (!await needsTtlExtension({ server, ledgerKey })) {
6846
+ return;
6847
+ }
6848
+ await submitExtendFootprintTtlTransaction({
6849
+ server,
6850
+ ledgerKey,
6851
+ networkPassphrase: parameters.networkPassphrase,
6852
+ sourcePublicKey: parameters.sourcePublicKey,
6853
+ signTransaction: parameters.signTransaction
6854
+ });
6855
+ }
6856
+
6671
6857
  // src/transact/submit/pool-transact.ts
6672
6858
  import { Buffer as Buffer17 } from "buffer";
6673
6859
  async function submitPoolTransact(parameters) {
6860
+ await extendZkConfigTtlIfNeeded({
6861
+ sorobanRpcUrl: parameters.sorobanRpcUrl,
6862
+ networkPassphrase: parameters.networkPassphrase,
6863
+ contractId: parameters.contractId,
6864
+ nonce: parameters.nonce,
6865
+ sourcePublicKey: parameters.from,
6866
+ signTransaction: parameters.transactEnvironment.signTransaction
6867
+ });
6674
6868
  const assembledTransaction = await parameters.contractClient.transact({
6675
6869
  from: parameters.from,
6870
+ nonce: parameters.nonce,
6676
6871
  proof_bytes: Buffer17.from(parameters.proofHex.replace(/^0x/iu, ""), "hex"),
6677
6872
  pub_signals_bytes: Buffer17.from(parameters.publicHex.replace(/^0x/iu, ""), "hex"),
6678
6873
  onboarding: parameters.onboarding,
@@ -6711,11 +6906,16 @@ async function submitSorobanConfidentialTransfer(input) {
6711
6906
  });
6712
6907
  return submitPoolTransact({
6713
6908
  contractClient: poolClient,
6909
+ contractId: input.contractId,
6714
6910
  from: input.walletPublicKey,
6911
+ nonce: resolveZkConfigNonce(input.transactEnvironment),
6715
6912
  proofHex: input.proofHex,
6716
6913
  publicHex: input.publicHex,
6717
6914
  ...input.onboarding ? { onboarding: input.onboarding } : {},
6718
- approval
6915
+ approval,
6916
+ networkPassphrase: input.networkPassphrase,
6917
+ sorobanRpcUrl: input.sorobanRpcUrl,
6918
+ transactEnvironment: input.transactEnvironment
6719
6919
  });
6720
6920
  }
6721
6921
 
@@ -6744,10 +6944,15 @@ async function submitPreparedDeposit(prepared, environment) {
6744
6944
  });
6745
6945
  return submitPoolTransact({
6746
6946
  contractClient: poolClient,
6947
+ contractId: environment.network.poolContract,
6747
6948
  from: artifacts.walletPublicKey,
6949
+ nonce: resolveZkConfigNonce(environment),
6748
6950
  proofHex: artifacts.proofHex,
6749
6951
  publicHex: artifacts.publicHex,
6750
- approval
6952
+ approval,
6953
+ networkPassphrase: environment.network.networkPassphrase,
6954
+ sorobanRpcUrl: environment.network.rpcUrl,
6955
+ transactEnvironment: environment
6751
6956
  });
6752
6957
  }
6753
6958
  async function submitPreparedPoolTransact(prepared, environment) {
@@ -6996,10 +7201,15 @@ async function submitSorobanDeposit(input) {
6996
7201
  });
6997
7202
  return submitPoolTransact({
6998
7203
  contractClient: poolClient,
7204
+ contractId: input.contractId,
6999
7205
  from: input.walletPublicKey,
7206
+ nonce: resolveZkConfigNonce(input.transactEnvironment),
7000
7207
  proofHex: proof_hex,
7001
7208
  publicHex: public_hex,
7002
- approval
7209
+ approval,
7210
+ networkPassphrase: input.networkPassphrase,
7211
+ sorobanRpcUrl: input.sorobanRpcUrl,
7212
+ transactEnvironment: input.transactEnvironment
7003
7213
  });
7004
7214
  }
7005
7215
  export {