@chironbuilder/ootle-sdk 0.1.14 → 0.1.15
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/burnClaim.d.ts +62 -20
- package/dist/burnClaim.d.ts.map +1 -1
- package/dist/burnClaim.js +201 -34
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/wallet.d.ts +27 -0
- package/dist/wallet.d.ts.map +1 -1
- package/dist/wallet.js +212 -41
- package/package.json +6 -2
- package/vendor/ootle-patched/dist/index.d.ts +57 -28
- package/vendor/ootle-patched/dist/index.js +1389 -1365
- package/vendor/ootle-patched/package.json +23 -23
- package/vendor/ootle-wasm-patched/ootle_wasm.d.ts +4 -4
- package/vendor/ootle-wasm-patched/ootle_wasm_bg.js +29 -17
- package/vendor/ootle-wasm-patched/ootle_wasm_bg.wasm +0 -0
- package/vendor/ootle-wasm-patched/ootle_wasm_bg.wasm.d.ts +4 -4
- package/vendor/ootle-wasm-patched/package.json +3 -4
package/dist/wallet.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Mask, OotleWallet, StealthInput, StealthInputsStatement, StealthOutputsStatement, StealthTransfer, StealthTransferStatement, TARI_RESOURCE_ADDRESS, TransactionBuilder, WalletStealthAuthorizer, WasmStealthCrypto, XTR_FAUCET_CLAIM_RESOURCE_ADDRESS, XTR_FAUCET_COMPONENT_ADDRESS, XTR_FAUCET_VAULT_ADDRESS, amountLiteral, createOutput, decryptOwnedUtxo, defaultIndexerUrl, getVaultIdsForAccount, resolveMaxEpoch, resolveTransaction, resourceAddressLiteral, sealTransaction, sendTransaction, serializeUnsignedTx, signTransaction, stealthUtxoSubstateId, submitTransaction, } from "@tari-project/ootle";
|
|
1
|
+
import { Mask, OotleWallet, StealthInput, StealthInputsStatement, StealthOutputsStatement, StealthTransfer, StealthTransferStatement, TARI_RESOURCE_ADDRESS, TransactionBuilder, WalletStealthAuthorizer, WasmStealthCrypto, XTR_FAUCET_CLAIM_RESOURCE_ADDRESS, XTR_FAUCET_COMPONENT_ADDRESS, XTR_FAUCET_VAULT_ADDRESS, amountLiteral, createOutput, decryptOwnedUtxo, defaultIndexerUrl, getVaultIdsForAccount, resolveMaxEpoch, resolveTransaction, resourceAddressLiteral, generateSealKeypair, sealTransaction, sendTransaction, serializeUnsignedTx, signTransaction, stealthUtxoSubstateId, submitTransaction, } from "@tari-project/ootle";
|
|
2
2
|
import { IndexerProvider } from "@tari-project/ootle-indexer";
|
|
3
3
|
import { SecretKeyWallet } from "@tari-project/ootle-secret-key-wallet";
|
|
4
4
|
import { buildScriptPathWitness, burnClaimStealthSecret, buildStealthTransferStatement, createConfidentialWithdrawProofLiteral, createStealthOutputWitness, parseOotleAddress, publicKeyFromSecretKey, schnorrSign, stealthDhSecret, validateBurnClaimOwnershipProof, validateStealthTransfer, } from "@tari-project/ootle-wasm";
|
|
@@ -171,6 +171,15 @@ export class OotleAccount {
|
|
|
171
171
|
async getWalletAddress() {
|
|
172
172
|
return this.signer.getAddress();
|
|
173
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* This account's owner public key, synchronously. Named as the receiver of every revealed
|
|
176
|
+
* stealth output this account's `StealthTransfer`s create (tari-ootle#2645): the engine only
|
|
177
|
+
* hands a revealed bucket to a key whose badge is in the transaction's auth scope, and those
|
|
178
|
+
* transfers are always signed with the account key.
|
|
179
|
+
*/
|
|
180
|
+
get accountPublicKey() {
|
|
181
|
+
return publicKeyFromSecretKey(this.ownerSecret);
|
|
182
|
+
}
|
|
174
183
|
async getPublicKey() {
|
|
175
184
|
return this.signer.getPublicKey();
|
|
176
185
|
}
|
|
@@ -781,7 +790,7 @@ export class OotleAccount {
|
|
|
781
790
|
// on-chain component address -- passing `account` here throws "Bech32 decode error" (confirmed
|
|
782
791
|
// live). See getWalletAddress()'s doc comment for why these two addresses are easy to conflate.
|
|
783
792
|
const walletAddress = await this.getWalletAddress();
|
|
784
|
-
const builder = new StealthTransfer(provider, resourceAddress)
|
|
793
|
+
const builder = new StealthTransfer(provider, resourceAddress).withRevealedReceiver(this.accountPublicKey)
|
|
785
794
|
// StealthTransfer.prepare() auto-adds the revealed account's own substate and any vault
|
|
786
795
|
// addresses embedded in its on-chain state, but never the resource's own substate -- fine
|
|
787
796
|
// for XTR (resource_0101...0101 is engine-special-cased and needs no lock), but any other
|
|
@@ -924,9 +933,15 @@ export class OotleAccount {
|
|
|
924
933
|
// See shield()'s comment: Output.destination needs the bech32m wallet address, not the
|
|
925
934
|
// on-chain component address.
|
|
926
935
|
const walletAddress = await this.getWalletAddress();
|
|
936
|
+
// An account that has only ever held private funds (e.g. a claimed L1 burn) has no component
|
|
937
|
+
// on-chain to withdraw dust from, deposit into, or pay a fee from. Its first unshield creates
|
|
938
|
+
// the account from the revealed funds instead, paying the fee from them too.
|
|
939
|
+
if (!(await substateExists(provider, account))) {
|
|
940
|
+
return await this.unshieldIntoNewAccount(provider, resourceAddress, revealedOutAmount, commitments, remainder, maxFee, memo);
|
|
941
|
+
}
|
|
927
942
|
// See shield()'s comment: the resource's own substate must be pinned explicitly -- prepare()
|
|
928
943
|
// never adds it on its own.
|
|
929
|
-
let builder = new StealthTransfer(provider, resourceAddress)
|
|
944
|
+
let builder = new StealthTransfer(provider, resourceAddress).withRevealedReceiver(this.accountPublicKey)
|
|
930
945
|
.withBuilder((b) => b.addInput({ substate_id: resourceAddress, version: null }))
|
|
931
946
|
.spendRevealedInput(account, dust);
|
|
932
947
|
for (const commitment of commitments) {
|
|
@@ -1014,7 +1029,7 @@ export class OotleAccount {
|
|
|
1014
1029
|
// stealth output back to this account -- the same proven-working shape -- alongside the real
|
|
1015
1030
|
// amount as a revealed bucket.
|
|
1016
1031
|
const dust = 1n;
|
|
1017
|
-
let builder = new StealthTransfer(provider, resourceAddress)
|
|
1032
|
+
let builder = new StealthTransfer(provider, resourceAddress).withRevealedReceiver(this.accountPublicKey)
|
|
1018
1033
|
.withBuilder((b) => b.addInput({ substate_id: resourceAddress, version: null }))
|
|
1019
1034
|
.spendRevealedInput(account, amount + dust)
|
|
1020
1035
|
.toStealthOutput(createOutput({ destination: walletAddress, amount: dust, resourceAddress }))
|
|
@@ -1082,11 +1097,8 @@ export class OotleAccount {
|
|
|
1082
1097
|
inputs: [{ commitment: commitmentHex, witness: "KeyPath" }],
|
|
1083
1098
|
revealed_amount: "0",
|
|
1084
1099
|
});
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
revealed_output_amount: revealedAmount.toString(),
|
|
1088
|
-
agg_range_proof: "",
|
|
1089
|
-
});
|
|
1100
|
+
// The account key signs this transaction (the authorizer's default), so it takes the revealed funds.
|
|
1101
|
+
const outputsJson = revealOnlyOutputsJson(revealedAmount, this.accountPublicKey);
|
|
1090
1102
|
const statement = new StealthTransferStatement(new StealthInputsStatement([], 0n, inputsJson), new StealthOutputsStatement(outputsJson));
|
|
1091
1103
|
const privateFee = await this.resolvePrivateFee(feeType, maxFee);
|
|
1092
1104
|
const maxEpoch = await resolveMaxEpoch(provider);
|
|
@@ -1237,7 +1249,9 @@ export class OotleAccount {
|
|
|
1237
1249
|
const feeChange = decrypted.value - maxFee;
|
|
1238
1250
|
const inputSkeleton = JSON.stringify({ inputs: [{ commitment: feeCommitmentHex, witness: "KeyPath" }], revealed_amount: "0" });
|
|
1239
1251
|
const feeChangeOutput = createOutput({ destination: walletAddress, amount: feeChange, resourceAddress: feeResourceAddress });
|
|
1240
|
-
const { statement: feeOutputsStatement, outputMask: feeOutputMask } = await crypto.generateOutputsStatement([feeChangeOutput], maxFee
|
|
1252
|
+
const { statement: feeOutputsStatement, outputMask: feeOutputMask } = await crypto.generateOutputsStatement([feeChangeOutput], maxFee,
|
|
1253
|
+
// The revealed fee goes to the fee UTXO's one-time key, which signs for it (see `feeSigner`).
|
|
1254
|
+
utxoSpendKey(substate));
|
|
1241
1255
|
const feeInputsStatement = new StealthInputsStatement([], 0n, inputSkeleton);
|
|
1242
1256
|
const feeAggInputMask = await crypto.aggregateInputMasks([decrypted.mask]);
|
|
1243
1257
|
const feeBalanceProof = await crypto.generateBalanceProofSignature(feeAggInputMask, feeOutputMask, feeInputsStatement.statementJson, feeOutputsStatement.statementJson);
|
|
@@ -1551,7 +1565,10 @@ export class OotleAccount {
|
|
|
1551
1565
|
// Redeemed resource: 1 stealth input -> 0 stealth outputs, fully revealed into the bucket
|
|
1552
1566
|
// `followUpInstructions` consumes.
|
|
1553
1567
|
const redeemedInputsStatement = new StealthInputsStatement([], 0n, inputSkeleton);
|
|
1554
|
-
const redeemedOutputsStatement = new StealthOutputsStatement(
|
|
1568
|
+
const redeemedOutputsStatement = new StealthOutputsStatement(
|
|
1569
|
+
// This transaction deliberately carries no account signature, so the revealed funds go to the
|
|
1570
|
+
// key that does sign for them: the redeemed UTXO's own one-time spend key.
|
|
1571
|
+
revealOnlyOutputsJson(revealedAmount, utxoSpendKey(substate)));
|
|
1555
1572
|
const redeemedAggInputMask = await crypto.aggregateInputMasks([redeemed.mask]);
|
|
1556
1573
|
const redeemedBalanceProof = await crypto.generateBalanceProofSignature(redeemedAggInputMask, Mask.zero(), redeemedInputsStatement.statementJson, redeemedOutputsStatement.statementJson);
|
|
1557
1574
|
const redeemedStatement = new StealthTransferStatement(redeemedInputsStatement, redeemedOutputsStatement, redeemedBalanceProof);
|
|
@@ -1666,7 +1683,7 @@ export class OotleAccount {
|
|
|
1666
1683
|
// See shield()'s comment: the resource's own substate must be pinned explicitly -- prepare()
|
|
1667
1684
|
// never adds it on its own. Output 0 is the HTLC; output 1 (only with change) is ours.
|
|
1668
1685
|
const build = () => {
|
|
1669
|
-
let b = new StealthTransfer(provider, resourceAddress).withBuilder((bb) => bb.addInput({ substate_id: resourceAddress, version: null }));
|
|
1686
|
+
let b = new StealthTransfer(provider, resourceAddress).withRevealedReceiver(this.accountPublicKey).withBuilder((bb) => bb.addInput({ substate_id: resourceAddress, version: null }));
|
|
1670
1687
|
if (stealth)
|
|
1671
1688
|
for (const c of commitments)
|
|
1672
1689
|
b = b.spendStealthInput(account, fromHex(c));
|
|
@@ -2165,7 +2182,7 @@ export class OotleAccount {
|
|
|
2165
2182
|
const ownWalletAddress = await this.getWalletAddress();
|
|
2166
2183
|
// See shield()'s comment: the resource's own substate must be pinned explicitly -- prepare()
|
|
2167
2184
|
// never adds it on its own.
|
|
2168
|
-
let builder = new StealthTransfer(provider, resourceAddress)
|
|
2185
|
+
let builder = new StealthTransfer(provider, resourceAddress).withRevealedReceiver(this.accountPublicKey)
|
|
2169
2186
|
.withBuilder((b) => b.addInput({ substate_id: resourceAddress, version: null }))
|
|
2170
2187
|
.spendRevealedInput(account, dust);
|
|
2171
2188
|
for (const commitment of commitments) {
|
|
@@ -2275,9 +2292,156 @@ export class OotleAccount {
|
|
|
2275
2292
|
async claimBurn(contents, maxFee, memo = "Burnt funds claimed from L1") {
|
|
2276
2293
|
if (maxFee !== undefined && maxFee <= 0n)
|
|
2277
2294
|
throw new Error(`claimBurn: maxFee must be > 0, got ${maxFee}`);
|
|
2295
|
+
const { buildClaim, value, provider } = await this.prepareBurnClaim(contents, memo);
|
|
2296
|
+
let fee = maxFee ?? (await this.estimateClaimFee(await buildClaim(claimFeeProbe(value), true)));
|
|
2297
|
+
let attempt = await buildClaim(fee, false);
|
|
2298
|
+
let transactionId = await submitTransaction(provider, attempt.envelope);
|
|
2299
|
+
let response;
|
|
2300
|
+
try {
|
|
2301
|
+
response = await withTimeout(pollTransactionResult(provider, transactionId), 90_000, "claiming the burn");
|
|
2302
|
+
}
|
|
2303
|
+
catch (e) {
|
|
2304
|
+
// A real run can meter a little differently from its dry run. When the rejection names the
|
|
2305
|
+
// fee it wanted, a measured fee is resubmitted once at exactly that; an explicit one is not.
|
|
2306
|
+
const required = maxFee === undefined ? requiredFeeFromRejection(e) : null;
|
|
2307
|
+
if (required === null || required <= fee)
|
|
2308
|
+
throw e;
|
|
2309
|
+
fee = required;
|
|
2310
|
+
attempt = await buildClaim(fee, false);
|
|
2311
|
+
transactionId = await submitTransaction(provider, attempt.envelope);
|
|
2312
|
+
response = await withTimeout(pollTransactionResult(provider, transactionId), 90_000, "claiming the burn");
|
|
2313
|
+
}
|
|
2314
|
+
await recordKnownVersions(response);
|
|
2315
|
+
await recordKnownShieldedOutput(localAccountId(this.index), TARI_RESOURCE_ADDRESS, attempt.ownCommitment, attempt.claimedAmount, transactionId, memo);
|
|
2316
|
+
return { transactionId, claimedAmount: attempt.claimedAmount, commitment: attempt.ownCommitment, fee };
|
|
2317
|
+
}
|
|
2318
|
+
/**
|
|
2319
|
+
* The fee `claimBurn(contents)` would pay, measured by dry-running the claim -- nothing is
|
|
2320
|
+
* submitted. The indexer's dry run checks everything about the claim except the burn's L1
|
|
2321
|
+
* inclusion (the burn output rules, its ownership proof, the claim-key signature), so this also
|
|
2322
|
+
* tells a caller whether the claim is well formed before anything is spent.
|
|
2323
|
+
*/
|
|
2324
|
+
async estimateClaimBurnFee(contents, memo = "Burnt funds claimed from L1") {
|
|
2325
|
+
const { buildClaim, value } = await this.prepareBurnClaim(contents, memo);
|
|
2326
|
+
return this.estimateClaimFee(await buildClaim(claimFeeProbe(value), true));
|
|
2327
|
+
}
|
|
2328
|
+
/**
|
|
2329
|
+
* `unshield()` for an account whose component doesn't exist on-chain yet. Everything runs in the
|
|
2330
|
+
* fee phase, the way Tari's own wallet unshields into a new account: the stealth inputs reveal
|
|
2331
|
+
* `amount + fee` (any excess returns as a stealth change output), `TakeFromBucket` splits off the
|
|
2332
|
+
* fee for `PayFeeFromBucket`, and `CreateAccount` creates this account from the rest. The fee is
|
|
2333
|
+
* measured with a dry run first, since a fee revealed from stealth funds is never refunded.
|
|
2334
|
+
*
|
|
2335
|
+
* The seal key is authorized as a signer and named as the revealed output's receiver
|
|
2336
|
+
* (tari-ootle#2645); each input's one-time key co-signs its key-path spend.
|
|
2337
|
+
*/
|
|
2338
|
+
async unshieldIntoNewAccount(provider, resourceAddress, amount, commitments, remainder, maxFee, memo) {
|
|
2339
|
+
if (resourceAddress !== TARI_RESOURCE_ADDRESS) {
|
|
2340
|
+
throw new Error("This account doesn't exist on-chain yet. Unshield some TARI first to create it, then unshield other resources.");
|
|
2341
|
+
}
|
|
2342
|
+
const accountId = localAccountId(this.index);
|
|
2343
|
+
const crypto = new WasmStealthCrypto(this.network);
|
|
2344
|
+
const viewSecret = await this.signer.getViewSecret();
|
|
2345
|
+
const walletAddress = await this.getWalletAddress();
|
|
2346
|
+
const inputs = await Promise.all(commitments.map(async (commitmentHex) => {
|
|
2347
|
+
const substateId = stealthUtxoSubstateId(resourceAddress, fromHex(commitmentHex));
|
|
2348
|
+
const substate = await provider.getSubstate(substateId);
|
|
2349
|
+
const decrypted = await decryptOwnedUtxo(crypto, viewSecret, substate, substateId);
|
|
2350
|
+
if (!decrypted)
|
|
2351
|
+
throw new Error(`unshield: cannot decrypt ${substateId} -- it isn't this account's, or it is already spent.`);
|
|
2352
|
+
const nonce = substate.substate.Utxo.output.output
|
|
2353
|
+
.public_nonce;
|
|
2354
|
+
return { commitmentHex, substateId, mask: decrypted.mask, nonce };
|
|
2355
|
+
}));
|
|
2356
|
+
const inputMask = await crypto.aggregateInputMasks(inputs.map((i) => i.mask));
|
|
2357
|
+
const inputsJson = JSON.stringify({
|
|
2358
|
+
inputs: inputs.map((i) => ({ commitment: i.commitmentHex, witness: "KeyPath" })),
|
|
2359
|
+
revealed_amount: "0",
|
|
2360
|
+
});
|
|
2361
|
+
const sealKeypair = generateSealKeypair();
|
|
2362
|
+
const build = async (fee, dryRun) => {
|
|
2363
|
+
// The fee comes out of the stealth side: what's revealed is `amount + fee`, and the change
|
|
2364
|
+
// output shrinks by `fee`.
|
|
2365
|
+
const change = remainder - fee;
|
|
2366
|
+
if (fee <= 0n || change < 0n) {
|
|
2367
|
+
throw new Error(`unshield: the private balance does not cover ${amount} plus the ${fee} fee to create the account`);
|
|
2368
|
+
}
|
|
2369
|
+
const revealed = amount + fee;
|
|
2370
|
+
let outputsStatement;
|
|
2371
|
+
let outputMask = Mask.zero();
|
|
2372
|
+
if (change > 0n) {
|
|
2373
|
+
const output = createOutput({ destination: walletAddress, amount: change, resourceAddress, memo: toMemo(memo) });
|
|
2374
|
+
({ statement: outputsStatement, outputMask } = await crypto.generateOutputsStatement([output], revealed, sealKeypair.public_key));
|
|
2375
|
+
}
|
|
2376
|
+
else {
|
|
2377
|
+
outputsStatement = new StealthOutputsStatement(revealOnlyOutputsJson(revealed, sealKeypair.public_key));
|
|
2378
|
+
}
|
|
2379
|
+
const inputsStatement = new StealthInputsStatement([], 0n, inputsJson);
|
|
2380
|
+
const balanceProof = await crypto.generateBalanceProofSignature(inputMask, outputMask, inputsJson, outputsStatement.statementJson);
|
|
2381
|
+
const statement = new StealthTransferStatement(inputsStatement, outputsStatement, balanceProof);
|
|
2382
|
+
await crypto.validateTransfer(statement);
|
|
2383
|
+
const changeCommitment = change > 0n ? outputsStatement.parsed().outputs[0].output.commitment : null;
|
|
2384
|
+
const builder = TransactionBuilder.new(this.network, await resolveMaxEpoch(provider));
|
|
2385
|
+
for (const instruction of [
|
|
2386
|
+
{
|
|
2387
|
+
StealthTransfer: {
|
|
2388
|
+
resource_address_ref: { Address: resourceAddress },
|
|
2389
|
+
statement: { __ootleRawJson: statement.toCompactJson() },
|
|
2390
|
+
revealed_input_bucket: null,
|
|
2391
|
+
},
|
|
2392
|
+
},
|
|
2393
|
+
{ PutLastInstructionOutputOnWorkspace: { key: 0 } },
|
|
2394
|
+
{ TakeFromBucket: { input_bucket: { id: 0, offset: null }, amount: fee.toString(), output_bucket: 1 } },
|
|
2395
|
+
{ PayFeeFromBucket: { bucket: { id: 1, offset: null } } },
|
|
2396
|
+
{
|
|
2397
|
+
CreateAccount: {
|
|
2398
|
+
owner_public_key: toHex(this.accountPublicKey),
|
|
2399
|
+
owner_rule: null,
|
|
2400
|
+
access_rules: null,
|
|
2401
|
+
bucket_workspace_id: { id: 0, offset: null },
|
|
2402
|
+
},
|
|
2403
|
+
},
|
|
2404
|
+
]) {
|
|
2405
|
+
builder.addFeeInstruction(instruction);
|
|
2406
|
+
}
|
|
2407
|
+
builder.addInput({ substate_id: resourceAddress, version: null, is_write: false });
|
|
2408
|
+
for (const input of inputs)
|
|
2409
|
+
builder.addInput({ substate_id: input.substateId, version: null });
|
|
2410
|
+
const unsignedBody = builder.buildUnsignedTransaction();
|
|
2411
|
+
unsignedBody.is_seal_signer_authorized = true;
|
|
2412
|
+
unsignedBody.dry_run = dryRun;
|
|
2413
|
+
const unsignedTx = await resolveTransaction(provider, unsignedBody);
|
|
2414
|
+
const signers = inputs.map((input) => ({
|
|
2415
|
+
getAddress: async () => walletAddress,
|
|
2416
|
+
getPublicKey: async () => parseOotleAddress(walletAddress).owner_key,
|
|
2417
|
+
signTransaction: async (tx, sealPublicKey) => {
|
|
2418
|
+
const json = serializeUnsignedTx(tx);
|
|
2419
|
+
return [await this.signer.addStealthSignature(json, fromHex(input.nonce), sealPublicKey, { crypto })];
|
|
2420
|
+
},
|
|
2421
|
+
}));
|
|
2422
|
+
const signed = await signTransaction(signers, unsignedTx, sealKeypair);
|
|
2423
|
+
return { envelope: sealTransaction(signed), change, changeCommitment };
|
|
2424
|
+
};
|
|
2425
|
+
const probe = maxFee < remainder ? maxFee : remainder;
|
|
2426
|
+
const fee = await this.estimateClaimFee(await build(probe, true), "unshield");
|
|
2427
|
+
if (fee > maxFee)
|
|
2428
|
+
throw new Error(`unshield: creating the account costs ${fee}, above the ${maxFee} limit`);
|
|
2429
|
+
const attempt = await build(fee, false);
|
|
2430
|
+
const transactionId = await submitTransaction(provider, attempt.envelope);
|
|
2431
|
+
const response = await withTimeout(pollTransactionResult(provider, transactionId), 90_000, "unshielding into a new account");
|
|
2432
|
+
await recordKnownVersions(response);
|
|
2433
|
+
for (const input of inputs)
|
|
2434
|
+
await markShieldedOutputSpent(accountId, input.commitmentHex);
|
|
2435
|
+
if (attempt.changeCommitment) {
|
|
2436
|
+
await recordKnownShieldedOutput(accountId, resourceAddress, attempt.changeCommitment, attempt.change, transactionId, memo);
|
|
2437
|
+
}
|
|
2438
|
+
return { transactionId };
|
|
2439
|
+
}
|
|
2440
|
+
/** Verifies a burn is ours and returns a builder for its claim transaction at a given fee. */
|
|
2441
|
+
async prepareBurnClaim(contents, memo) {
|
|
2278
2442
|
const proof = contents.claim_proof;
|
|
2279
2443
|
const burnCommitment = fromHex(proof.commitment);
|
|
2280
|
-
const senderOffset = fromHex(proof.sender_offset_public_key);
|
|
2444
|
+
const senderOffset = fromHex(proof.output.sender_offset_public_key);
|
|
2281
2445
|
const value = BigInt(proof.value);
|
|
2282
2446
|
const stealthSecret = burnClaimStealthSecret(this.ownerSecret, senderOffset);
|
|
2283
2447
|
const ownershipValid = validateBurnClaimOwnershipProof(this.network, fromHex(proof.ownership_proof.public_nonce), fromHex(proof.ownership_proof.signature), burnCommitment, value, stealthSecret);
|
|
@@ -2292,6 +2456,7 @@ export class OotleAccount {
|
|
|
2292
2456
|
}
|
|
2293
2457
|
const provider = await this.getProvider();
|
|
2294
2458
|
const walletAddress = await this.getWalletAddress();
|
|
2459
|
+
// The claim's only signer, so it is also the receiver of the revealed fee (tari-ootle#2645).
|
|
2295
2460
|
const sealKeypair = { secret_key: stealthSecret, public_key: publicKeyFromSecretKey(stealthSecret) };
|
|
2296
2461
|
// One claim transaction revealing `fee`. The output statement encodes the revealed amount, so
|
|
2297
2462
|
// every fee needs its own statement and proofs.
|
|
@@ -2301,7 +2466,7 @@ export class OotleAccount {
|
|
|
2301
2466
|
throw new Error(`claimBurn: the burn (${decrypted.value}) does not cover the claim fee (${fee})`);
|
|
2302
2467
|
}
|
|
2303
2468
|
const output = createOutput({ destination: walletAddress, amount: claimedAmount, resourceAddress: TARI_RESOURCE_ADDRESS, memo: toMemo(memo) });
|
|
2304
|
-
const { statement: outputsStatement, outputMask } = await crypto.generateOutputsStatement([output], fee);
|
|
2469
|
+
const { statement: outputsStatement, outputMask } = await crypto.generateOutputsStatement([output], fee, sealKeypair.public_key);
|
|
2305
2470
|
// The single input is the UTXO `ClaimBurn` mints in the same transaction; the instruction
|
|
2306
2471
|
// itself authorises spending it, so it carries no witness.
|
|
2307
2472
|
const inputsStatement = await crypto.buildInputsStatement([new StealthInput(burnCommitment)], 0n);
|
|
@@ -2322,7 +2487,7 @@ export class OotleAccount {
|
|
|
2322
2487
|
})
|
|
2323
2488
|
.addFeeInstruction({ PutLastInstructionOutputOnWorkspace: { key: 0 } })
|
|
2324
2489
|
.addFeeInstruction({ PayFeeFromBucket: { bucket: { id: 0, offset: null } } })
|
|
2325
|
-
.addInput({ substate_id: TARI_RESOURCE_ADDRESS, version: null });
|
|
2490
|
+
.addInput({ substate_id: TARI_RESOURCE_ADDRESS, version: null, is_write: false });
|
|
2326
2491
|
// The seal key `s` is the transaction's only signer, so it must be authorized as the main
|
|
2327
2492
|
// signer; the TS builder leaves that off by default.
|
|
2328
2493
|
const unsignedBody = builder.buildUnsignedTransaction();
|
|
@@ -2332,30 +2497,10 @@ export class OotleAccount {
|
|
|
2332
2497
|
const signed = await signTransaction([], unsignedTx, sealKeypair);
|
|
2333
2498
|
return { envelope: sealTransaction(signed), claimedAmount, ownCommitment };
|
|
2334
2499
|
};
|
|
2335
|
-
|
|
2336
|
-
let attempt = await buildClaim(fee, false);
|
|
2337
|
-
let transactionId = await submitTransaction(provider, attempt.envelope);
|
|
2338
|
-
let response;
|
|
2339
|
-
try {
|
|
2340
|
-
response = await withTimeout(pollTransactionResult(provider, transactionId), 90_000, "claiming the burn");
|
|
2341
|
-
}
|
|
2342
|
-
catch (e) {
|
|
2343
|
-
// A real run can meter a little differently from its dry run. When the rejection names the
|
|
2344
|
-
// fee it wanted, a measured fee is resubmitted once at exactly that; an explicit one is not.
|
|
2345
|
-
const required = maxFee === undefined ? requiredFeeFromRejection(e) : null;
|
|
2346
|
-
if (required === null || required <= fee)
|
|
2347
|
-
throw e;
|
|
2348
|
-
fee = required;
|
|
2349
|
-
attempt = await buildClaim(fee, false);
|
|
2350
|
-
transactionId = await submitTransaction(provider, attempt.envelope);
|
|
2351
|
-
response = await withTimeout(pollTransactionResult(provider, transactionId), 90_000, "claiming the burn");
|
|
2352
|
-
}
|
|
2353
|
-
await recordKnownVersions(response);
|
|
2354
|
-
await recordKnownShieldedOutput(localAccountId(this.index), TARI_RESOURCE_ADDRESS, attempt.ownCommitment, attempt.claimedAmount, transactionId, memo);
|
|
2355
|
-
return { transactionId, claimedAmount: attempt.claimedAmount, commitment: attempt.ownCommitment, fee };
|
|
2500
|
+
return { buildClaim, value: decrypted.value, provider };
|
|
2356
2501
|
}
|
|
2357
2502
|
/** Dry-runs a sealed claim and returns the fee it needs: what it was charged plus the allowance. */
|
|
2358
|
-
async estimateClaimFee(dryRun) {
|
|
2503
|
+
async estimateClaimFee(dryRun, what = "claim") {
|
|
2359
2504
|
const res = await fetch(`${defaultIndexerUrl(this.network)}/transactions/dry-run`, {
|
|
2360
2505
|
method: "POST",
|
|
2361
2506
|
headers: { "Content-Type": "application/json" },
|
|
@@ -2370,7 +2515,7 @@ export class OotleAccount {
|
|
|
2370
2515
|
// Not JSON — reported raw below.
|
|
2371
2516
|
}
|
|
2372
2517
|
if (!res.ok || body?.error || !body?.result) {
|
|
2373
|
-
throw new Error(`Could not estimate the
|
|
2518
|
+
throw new Error(`Could not estimate the ${what} fee: ${body?.error?.message ?? text ?? res.statusText}`);
|
|
2374
2519
|
}
|
|
2375
2520
|
const finalize = body.result.finalize;
|
|
2376
2521
|
const outcome = finalize.result;
|
|
@@ -2383,7 +2528,7 @@ export class OotleAccount {
|
|
|
2383
2528
|
if (amount !== undefined)
|
|
2384
2529
|
charged += BigInt(amount);
|
|
2385
2530
|
if (charged === 0n)
|
|
2386
|
-
throw new Error(
|
|
2531
|
+
throw new Error(`Could not estimate the ${what} fee: the dry run reported no cost`);
|
|
2387
2532
|
return charged + FEE_ESTIMATE_ALLOWANCE;
|
|
2388
2533
|
}
|
|
2389
2534
|
/**
|
|
@@ -2648,7 +2793,7 @@ export function buildHtlcSpendStatement(params) {
|
|
|
2648
2793
|
const destination = parseOotleAddress(params.destinationWalletAddress);
|
|
2649
2794
|
const outputWitnessJson = createStealthOutputWitness(params.network, destination.owner_key, destination.view_key, params.value, params.resourceAddress, null, null, null, // pay_to_json: null -- default StealthPublicKey, a normal one-time key-spendable output.
|
|
2650
2795
|
0n);
|
|
2651
|
-
const statementJson = buildStealthTransferStatement(`[${inputEntry}]`, 0n, `[${outputWitnessJson}]`, 0n);
|
|
2796
|
+
const statementJson = buildStealthTransferStatement(`[${inputEntry}]`, 0n, `[${outputWitnessJson}]`, 0n, new Uint8Array(0));
|
|
2652
2797
|
// Fail fast locally rather than spend a network round trip on a malformed statement -- the same
|
|
2653
2798
|
// check tari-ootle's own `build_stealth_transfer_statement` unit tests run before trusting its
|
|
2654
2799
|
// output (crates/ootle_wasm/core/src/stealth/transfer.rs).
|
|
@@ -3091,6 +3236,14 @@ export async function pollTransactionResult(provider, transactionId, timeoutMs =
|
|
|
3091
3236
|
if ("Rejected" in result) {
|
|
3092
3237
|
throw new Error(`Transaction ${transactionId} was rejected: ${result.Rejected.details}`);
|
|
3093
3238
|
}
|
|
3239
|
+
// Consensus can abort a transaction whatever its execution said (a lock conflict, an expired
|
|
3240
|
+
// epoch, too little fee); only a `Commit` means it happened.
|
|
3241
|
+
const decision = result.Finalized.final_decision;
|
|
3242
|
+
if (decision !== "Commit") {
|
|
3243
|
+
const reason = typeof decision === "object" && decision && "Abort" in decision ? decision.Abort : JSON.stringify(decision);
|
|
3244
|
+
const details = result.Finalized.abort_details ? `: ${result.Finalized.abort_details}` : "";
|
|
3245
|
+
throw new Error(`Transaction ${transactionId} was aborted (${reason})${details}`);
|
|
3246
|
+
}
|
|
3094
3247
|
const outcome = result.Finalized.execution_result?.finalize.result;
|
|
3095
3248
|
if (outcome && typeof outcome === "object") {
|
|
3096
3249
|
if ("Reject" in outcome) {
|
|
@@ -3233,3 +3386,21 @@ function describeSignedTx(signed) {
|
|
|
3233
3386
|
return "unavailable";
|
|
3234
3387
|
}
|
|
3235
3388
|
}
|
|
3389
|
+
/**
|
|
3390
|
+
* The one-time spend key of a stealth UTXO substate (its `auth` `Key`, or `KeyAndScript.spend_key`).
|
|
3391
|
+
* A key-path spend of the UTXO is signed with this key, so its badge is in the transaction's auth
|
|
3392
|
+
* scope -- which makes it a valid receiver for a revealed output (tari-ootle#2645) in transactions
|
|
3393
|
+
* this account deliberately doesn't sign with its own key.
|
|
3394
|
+
*/
|
|
3395
|
+
function utxoSpendKey(substate) {
|
|
3396
|
+
const auth = substate.substate?.Utxo?.output?.auth;
|
|
3397
|
+
const key = auth && "Key" in auth ? auth.Key : auth && "KeyAndScript" in auth ? auth.KeyAndScript.spend_key : undefined;
|
|
3398
|
+
if (typeof key !== "string" || !/^[0-9a-f]{64}$/i.test(key)) {
|
|
3399
|
+
throw new Error("This stealth output has no one-time spend key (it is script-only), so it can't be spent by key.");
|
|
3400
|
+
}
|
|
3401
|
+
return fromHex(key);
|
|
3402
|
+
}
|
|
3403
|
+
/** A hand-built outputs statement that reveals `amount` to `receiver` and creates no stealth outputs. */
|
|
3404
|
+
function revealOnlyOutputsJson(amount, receiver) {
|
|
3405
|
+
return JSON.stringify({ outputs: [], revealed_output: { amount: amount.toString(), receiver: toHex(receiver) }, agg_range_proof: "" });
|
|
3406
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chironbuilder/ootle-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Shared TypeScript client for Tari Ootle (L2): account derivation, balances, transactions, HTLCs, and stealth/confidential transfers — the account core behind Sapient and the Tari L1 web wallet's Ootle support.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -44,12 +44,16 @@
|
|
|
44
44
|
"@tari-project/ootle": "file:./vendor/ootle-patched",
|
|
45
45
|
"@tari-project/ootle-indexer": "^0.3.0",
|
|
46
46
|
"@tari-project/ootle-secret-key-wallet": "^0.3.0",
|
|
47
|
-
"@tari-project/ootle-ts-bindings": "^1.
|
|
47
|
+
"@tari-project/ootle-ts-bindings": "^1.55.0",
|
|
48
48
|
"@tari-project/ootle-wasm": "file:./vendor/ootle-wasm-patched"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/chrome": "^0.0.280",
|
|
52
52
|
"typescript": "^5.6.0",
|
|
53
53
|
"vitest": "^4.1.0"
|
|
54
|
+
},
|
|
55
|
+
"overrides": {
|
|
56
|
+
"@tari-project/ootle-wasm": "$@tari-project/ootle-wasm",
|
|
57
|
+
"@tari-project/ootle": "$@tari-project/ootle"
|
|
54
58
|
}
|
|
55
59
|
}
|
|
@@ -17,6 +17,7 @@ import { IndexerGetTransactionResultResponse } from '@tari-project/ootle-ts-bind
|
|
|
17
17
|
import { IndexerSubmitTransactionRequest } from '@tari-project/ootle-ts-bindings';
|
|
18
18
|
import { IndexerSubmitTransactionResponse } from '@tari-project/ootle-ts-bindings';
|
|
19
19
|
import { IndexerTransactionFinalizedResult } from '@tari-project/ootle-ts-bindings';
|
|
20
|
+
import { InputDeclaration } from '@tari-project/ootle-ts-bindings';
|
|
20
21
|
import { Instruction } from '@tari-project/ootle-ts-bindings';
|
|
21
22
|
import { InstructionArg } from '@tari-project/ootle-ts-bindings';
|
|
22
23
|
import { ListRecentTransactionsRequest } from '@tari-project/ootle-ts-bindings';
|
|
@@ -517,7 +518,7 @@ export declare function fromHexStr(hex: string): Uint8Array;
|
|
|
517
518
|
* @returns A complete transfer statement.
|
|
518
519
|
* @throws {InvalidArgumentError} if `specs` is empty.
|
|
519
520
|
*/
|
|
520
|
-
export declare function generateOutputsStatement(crypto: StealthCryptoProvider, specs: Output[], revealed: bigint): Promise<StealthTransferStatement>;
|
|
521
|
+
export declare function generateOutputsStatement(crypto: StealthCryptoProvider, specs: Output[], revealed: bigint, revealedReceiver?: Uint8Array): Promise<StealthTransferStatement>;
|
|
521
522
|
|
|
522
523
|
/**
|
|
523
524
|
* Generate a fresh seal keypair (a thin wrapper over the WASM `generateKeypair`).
|
|
@@ -1195,10 +1196,13 @@ export declare interface StealthCryptoProvider {
|
|
|
1195
1196
|
*
|
|
1196
1197
|
* @param specs - The outputs to create (incl. change).
|
|
1197
1198
|
* @param revealedOutputAmount - The un-confidential (revealed) µTari output amount.
|
|
1199
|
+
* @param revealedReceiver - The 32-byte public key allowed to take the revealed output
|
|
1200
|
+
* (tari-ootle#2645: the engine requires its badge in the transaction's auth scope). Required
|
|
1201
|
+
* when `revealedOutputAmount > 0`, ignored otherwise.
|
|
1198
1202
|
* @returns The WASM-produced {@link StealthOutputsStatement} and the aggregated
|
|
1199
1203
|
* `outputMask` for the balance proof.
|
|
1200
1204
|
*/
|
|
1201
|
-
generateOutputsStatement(specs: Output[], revealedOutputAmount: bigint): Promise<{
|
|
1205
|
+
generateOutputsStatement(specs: Output[], revealedOutputAmount: bigint, revealedReceiver?: Uint8Array): Promise<{
|
|
1202
1206
|
statement: StealthOutputsStatement;
|
|
1203
1207
|
outputMask: Mask;
|
|
1204
1208
|
}>;
|
|
@@ -1363,11 +1367,23 @@ export declare class StealthTransfer {
|
|
|
1363
1367
|
/** Guards against a second {@link prepare} re-emitting instructions into the same builder. */
|
|
1364
1368
|
private prepared;
|
|
1365
1369
|
/**
|
|
1366
|
-
*
|
|
1367
|
-
*
|
|
1368
|
-
*
|
|
1370
|
+
* When set, revealed change is captured as a bucket on the workspace under this name
|
|
1371
|
+
* instead of being auto-deposited back to `revealedInput.source` — see
|
|
1372
|
+
* {@link toRevealedOutputAsBucket}.
|
|
1369
1373
|
*/
|
|
1370
1374
|
private revealedOutputBucketVar;
|
|
1375
|
+
/**
|
|
1376
|
+
* Extra instructions appended after the native `StealthTransfer` instruction (and its
|
|
1377
|
+
* change-bucket handling), in the same transaction — see {@link andThen}. Lets a caller
|
|
1378
|
+
* chain e.g. a custom contract call that consumes the bucket saved by
|
|
1379
|
+
* {@link toRevealedOutputAsBucket}, all signed together by the authorizer in one
|
|
1380
|
+
* transaction instead of needing a separate one.
|
|
1381
|
+
*/
|
|
1382
|
+
private followUpInstructions;
|
|
1383
|
+
/**
|
|
1384
|
+
* The public key allowed to take the revealed output — see {@link withRevealedReceiver}.
|
|
1385
|
+
*/
|
|
1386
|
+
private revealedReceiver;
|
|
1371
1387
|
/**
|
|
1372
1388
|
* @param provider - Read-only chain access (used by {@link prepare} to resolve inputs).
|
|
1373
1389
|
* @param resourceAddress - The resource being transferred.
|
|
@@ -1394,32 +1410,40 @@ export declare class StealthTransfer {
|
|
|
1394
1410
|
*/
|
|
1395
1411
|
toRevealedOutput(amount: bigint): this;
|
|
1396
1412
|
/**
|
|
1397
|
-
* Like {@link toRevealedOutput}, but
|
|
1398
|
-
* `
|
|
1399
|
-
*
|
|
1400
|
-
*
|
|
1401
|
-
*
|
|
1402
|
-
*
|
|
1413
|
+
* Like {@link toRevealedOutput}, but instead of auto-depositing the change back into
|
|
1414
|
+
* `revealedInput.source`, the `StealthTransfer` instruction's bucket output is left on the
|
|
1415
|
+
* workspace under `workspaceVarName` for {@link andThen}'s instructions (or a caller-supplied
|
|
1416
|
+
* `withBuilder` continuation) to consume directly — e.g. passing it straight into a custom
|
|
1417
|
+
* contract call in the same transaction, rather than requiring a second transaction that
|
|
1418
|
+
* withdraws it back out again (which a plain `CallMethod withdraw` on a Stealth-typed vault
|
|
1419
|
+
* cannot do standalone — see this class's own `emitInstructions` doc comment: the account
|
|
1420
|
+
* template's `withdraw` only produces a valid bucket when paired with the native
|
|
1421
|
+
* `StealthTransfer` instruction in the same transaction).
|
|
1403
1422
|
*
|
|
1404
|
-
*
|
|
1405
|
-
*
|
|
1423
|
+
* A real `toStealthOutput` is still required elsewhere on this builder (`validate()` enforces
|
|
1424
|
+
* it): the bundled `ootle-wasm@0.37.0` signer cannot parse a statement with zero stealth
|
|
1425
|
+
* outputs at all — confirmed live, `signTransaction`'s own WASM call throws the generic "did
|
|
1426
|
+
* not match any variant of untagged enum TransactionInput" error regardless of how
|
|
1427
|
+
* `balance_proof` is represented. A small dust self-output (as `OotleAccount.
|
|
1428
|
+
* withdrawStealthAndExecute` in tari-wallet-extension does) satisfies this cheaply.
|
|
1406
1429
|
*
|
|
1407
|
-
* @throws {InvalidArgumentError} if `amount` is not `> 0
|
|
1408
|
-
* already set by an earlier call.
|
|
1430
|
+
* @throws {InvalidArgumentError} if `amount` is not `> 0`.
|
|
1409
1431
|
*/
|
|
1410
1432
|
toRevealedOutputAsBucket(amount: bigint, workspaceVarName: string): this;
|
|
1411
1433
|
/**
|
|
1412
|
-
*
|
|
1413
|
-
*
|
|
1414
|
-
*
|
|
1415
|
-
*
|
|
1434
|
+
* Name the public key allowed to take the revealed output (tari-ootle#2645). The engine
|
|
1435
|
+
* requires that key's badge in the transaction's auth scope before it creates the revealed
|
|
1436
|
+
* bucket, so it must be a key that signs the transaction — normally the account key, which
|
|
1437
|
+
* {@link WalletStealthAuthorizer} signs with by default. Required whenever the transfer has a
|
|
1438
|
+
* revealed output ({@link toRevealedOutput} / {@link toRevealedOutputAsBucket}).
|
|
1416
1439
|
*
|
|
1417
|
-
*
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
*
|
|
1422
|
-
*
|
|
1440
|
+
* @throws {InvalidArgumentError} if `publicKey` is not 32 bytes.
|
|
1441
|
+
*/
|
|
1442
|
+
withRevealedReceiver(publicKey: Uint8Array): this;
|
|
1443
|
+
/**
|
|
1444
|
+
* Append `instructions` after the native `StealthTransfer` instruction (and its bucket
|
|
1445
|
+
* handling), so they run in the same signed transaction — typically consuming the bucket
|
|
1446
|
+
* {@link toRevealedOutputAsBucket} saved to the workspace. Repeatable; each call appends.
|
|
1423
1447
|
*/
|
|
1424
1448
|
andThen(instructions: Instruction[]): this;
|
|
1425
1449
|
/**
|
|
@@ -1840,8 +1864,13 @@ export declare class TransactionBuilder {
|
|
|
1840
1864
|
* Note this REPLACES any fee instructions already set, as it always has.
|
|
1841
1865
|
*/
|
|
1842
1866
|
withFeeInstructionsBuilder(builder: (b: TransactionBuilder) => TransactionBuilder): this;
|
|
1843
|
-
|
|
1844
|
-
|
|
1867
|
+
/**
|
|
1868
|
+
* Declare a transaction input. Since tari-ootle#2640 every input declares read or write
|
|
1869
|
+
* intent; a plain {@link SubstateRequirement} (no `is_write`) is declared a **write**, the
|
|
1870
|
+
* engine's own default — over-declaring only locks more, under-declaring aborts execution.
|
|
1871
|
+
*/
|
|
1872
|
+
addInput(input: SubstateRequirement | InputDeclaration): this;
|
|
1873
|
+
withInputs(inputs: (SubstateRequirement | InputDeclaration)[]): this;
|
|
1845
1874
|
/** @throws {InvalidArgumentError} if `minEpoch` is not a non-negative integer. */
|
|
1846
1875
|
withMinEpoch(minEpoch: number): this;
|
|
1847
1876
|
/**
|
|
@@ -2076,7 +2105,7 @@ export declare class WasmStealthCrypto implements StealthCryptoProvider {
|
|
|
2076
2105
|
*/
|
|
2077
2106
|
private readonly network;
|
|
2078
2107
|
constructor(network?: Network);
|
|
2079
|
-
generateOutputsStatement(specs: Output[], revealedOutputAmount: bigint): Promise<{
|
|
2108
|
+
generateOutputsStatement(specs: Output[], revealedOutputAmount: bigint, revealedReceiver?: Uint8Array): Promise<{
|
|
2080
2109
|
statement: StealthOutputsStatement;
|
|
2081
2110
|
outputMask: Mask;
|
|
2082
2111
|
}>;
|