@chironbuilder/ootle-sdk 0.1.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/LICENSE +21 -0
- package/README.md +90 -0
- package/dist/accountApi.d.ts +33 -0
- package/dist/accountApi.d.ts.map +1 -0
- package/dist/accountApi.js +1 -0
- package/dist/adapters.d.ts +13 -0
- package/dist/adapters.d.ts.map +1 -0
- package/dist/adapters.js +68 -0
- package/dist/componentAddress.d.ts +27 -0
- package/dist/componentAddress.d.ts.map +1 -0
- package/dist/componentAddress.js +68 -0
- package/dist/confidential.d.ts +50 -0
- package/dist/confidential.d.ts.map +1 -0
- package/dist/confidential.js +86 -0
- package/dist/derivation.d.ts +7 -0
- package/dist/derivation.d.ts.map +1 -0
- package/dist/derivation.js +47 -0
- package/dist/domainHash.d.ts +11 -0
- package/dist/domainHash.d.ts.map +1 -0
- package/dist/domainHash.js +44 -0
- package/dist/htlc.d.ts +26 -0
- package/dist/htlc.d.ts.map +1 -0
- package/dist/htlc.js +69 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/ootleNetwork.d.ts +4 -0
- package/dist/ootleNetwork.d.ts.map +1 -0
- package/dist/ootleNetwork.js +11 -0
- package/dist/ownershipProof.d.ts +4 -0
- package/dist/ownershipProof.d.ts.map +1 -0
- package/dist/ownershipProof.js +53 -0
- package/dist/storage.d.ts +61 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +110 -0
- package/dist/timeout.d.ts +2 -0
- package/dist/timeout.d.ts.map +1 -0
- package/dist/timeout.js +19 -0
- package/dist/vault.d.ts +12 -0
- package/dist/vault.d.ts.map +1 -0
- package/dist/vault.js +44 -0
- package/dist/wallet.d.ts +733 -0
- package/dist/wallet.d.ts.map +1 -0
- package/dist/wallet.js +1964 -0
- package/package.json +46 -0
- package/vendor/ootle-patched/README.md +67 -0
- package/vendor/ootle-patched/dist/index.d.ts +2129 -0
- package/vendor/ootle-patched/dist/index.js +1365 -0
- package/vendor/ootle-patched/package.json +23 -0
- package/vendor/ootle-wasm-patched/README.md +128 -0
- package/vendor/ootle-wasm-patched/ootle_wasm.d.ts +454 -0
- package/vendor/ootle-wasm-patched/ootle_wasm.js +9 -0
- package/vendor/ootle-wasm-patched/ootle_wasm_bg.js +1485 -0
- package/vendor/ootle-wasm-patched/ootle_wasm_bg.wasm +0 -0
- package/vendor/ootle-wasm-patched/ootle_wasm_bg.wasm.d.ts +83 -0
- package/vendor/ootle-wasm-patched/package.json +27 -0
package/dist/htlc.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// HTLC (hashed timelock contract) condition-tree helpers for stealth outputs, built on TIP-0006
|
|
2
|
+
// script-path spending (`PayTo::Conditions`). See tari-project/tari-ootle#2426 and #2431 for the
|
|
3
|
+
// underlying wasm capability this builds on (buildScriptPathWitness/buildStealthTransferStatement,
|
|
4
|
+
// published in @tari-project/ootle-wasm@0.39.0).
|
|
5
|
+
//
|
|
6
|
+
// The exact nested shape here (`AccessRule`/`RestrictedAccessRule`/`RequireRule`/`RuleRequirement`/
|
|
7
|
+
// `NonFungibleAddress`) mirrors `tari-ootle`'s own reference implementation
|
|
8
|
+
// (`applications/tari_walletd/src/handlers/accounts.rs`'s `htlc_conditions`/`requires_public_key`
|
|
9
|
+
// test helpers) and is cross-verified byte-for-byte against a real Rust-generated golden vector in
|
|
10
|
+
// htlc.test.ts -- not hand-derived from docs alone. Getting this JSON shape wrong doesn't fail
|
|
11
|
+
// loudly: it produces a condition tree that either can't be claimed by the intended party or can be
|
|
12
|
+
// claimed by the wrong one, so treat any change here as fund-safety-critical.
|
|
13
|
+
/** The well-known, protocol-fixed resource address for public-key-derived "virtual" ownership
|
|
14
|
+
* badges (`PUBLIC_IDENTITY_RESOURCE_ADDRESS` in `tari_template_lib_types::constants`). Presenting a
|
|
15
|
+
* proof of the non-fungible `{resource_address: this, id: {U256: pubkeyHex}}` -- which the engine
|
|
16
|
+
* auto-populates for any signer of the transaction -- is how "signed by this public key" is
|
|
17
|
+
* expressed as an `AccessRule` in this engine, unlike Bitcoin-style direct signature checks. */
|
|
18
|
+
const PUBLIC_IDENTITY_RESOURCE_ADDRESS = "resource_0100000000000000000000000000000000000000000000000000000000000000";
|
|
19
|
+
/** An `AccessRule` satisfied only by a transaction signed with `publicKeyHex` (32 raw bytes, hex). */
|
|
20
|
+
export function accessRuleRequiringPublicKey(publicKeyHex) {
|
|
21
|
+
return {
|
|
22
|
+
Restricted: {
|
|
23
|
+
Require: {
|
|
24
|
+
Require: {
|
|
25
|
+
NonFungibleAddress: {
|
|
26
|
+
resource_address: PUBLIC_IDENTITY_RESOURCE_ADDRESS,
|
|
27
|
+
id: { U256: publicKeyHex },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** The two-leaf HTLC condition tree, `[claimLeaf, refundLeaf]` -- pass directly as
|
|
35
|
+
* `createOutput({ payTo: { Conditions: htlcConditions(...) }, ... })`.
|
|
36
|
+
*
|
|
37
|
+
* Claim leaf: hashlock AND before the refund epoch AND the claimant's key.
|
|
38
|
+
* Refund leaf: at/after the refund epoch AND the refunder's key.
|
|
39
|
+
*
|
|
40
|
+
* The condition tree's root (computed on-chain from these leaves at output-creation time) is all
|
|
41
|
+
* that's committed publicly; these full leaves must be handed to the claimant out of band (e.g. by
|
|
42
|
+
* the swap protocol/dApp) so they can later reveal the claim leaf via `buildScriptPathWitness`. */
|
|
43
|
+
export function htlcConditions(params) {
|
|
44
|
+
// The hash is the SHA-256 digest of the secret preimage: exactly 32 bytes (64 hex chars). A
|
|
45
|
+
// malformed value here silently produces a claim path that can never be satisfied by the
|
|
46
|
+
// intended claimant, so reject it up front rather than funding an unclaimable output. Hex is
|
|
47
|
+
// case-insensitive; normalize to lowercase for a canonical, deterministic hash string.
|
|
48
|
+
if (!/^[0-9a-f]{64}$/i.test(params.hashLockHex)) {
|
|
49
|
+
throw new Error(`htlcConditions: hashLockHex must be exactly 64 hex characters (32 bytes), got ${JSON.stringify(params.hashLockHex)}`);
|
|
50
|
+
}
|
|
51
|
+
if (!Number.isSafeInteger(Number(params.refundEpoch))) {
|
|
52
|
+
throw new Error(`htlcConditions: refundEpoch ${params.refundEpoch} is not representable as a safe JS integer`);
|
|
53
|
+
}
|
|
54
|
+
if (params.refundEpoch <= 0n) {
|
|
55
|
+
throw new Error(`htlcConditions: refundEpoch must be a positive epoch, got ${params.refundEpoch}`);
|
|
56
|
+
}
|
|
57
|
+
const hashLockHex = params.hashLockHex.toLowerCase();
|
|
58
|
+
const refundEpoch = Number(params.refundEpoch);
|
|
59
|
+
const claim = [
|
|
60
|
+
{ Builtin: { HashLock: { hash: hashLockHex, alg: "Sha256" } } },
|
|
61
|
+
{ Builtin: { BeforeEpoch: refundEpoch } },
|
|
62
|
+
{ AccessRule: accessRuleRequiringPublicKey(params.claimantPublicKeyHex) },
|
|
63
|
+
];
|
|
64
|
+
const refund = [
|
|
65
|
+
{ Builtin: { AfterEpoch: refundEpoch } },
|
|
66
|
+
{ AccessRule: accessRuleRequiringPublicKey(params.refunderPublicKeyHex) },
|
|
67
|
+
];
|
|
68
|
+
return [claim, refund];
|
|
69
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type { TransactionExecuteOpts, WalletAccountApi } from "./accountApi";
|
|
2
|
+
export { ACCOUNT_TEMPLATE_ADDRESS, componentAddressFromWalletAddress, deriveAccountComponentAddress, deriveComponentAddress, } from "./componentAddress";
|
|
3
|
+
export type { ConfidentialSumResult, ScannedStealthOutput } from "./confidential";
|
|
4
|
+
export { scanTransactionsForOwnedOutputs, sumConfidentialCommitments } from "./confidential";
|
|
5
|
+
export type { DerivedAccountKeys } from "./derivation";
|
|
6
|
+
export { deriveAccountKeys } from "./derivation";
|
|
7
|
+
export { DomainSeparatedHasher, KEY_MANAGER_DOMAIN, KEY_MANAGER_DOMAIN_VERSION, keyManagerDomainHasher } from "./domainHash";
|
|
8
|
+
export type { HtlcConditionsParams } from "./htlc";
|
|
9
|
+
export { accessRuleRequiringPublicKey, htlcConditions } from "./htlc";
|
|
10
|
+
export type { NetworkName } from "./ootleNetwork";
|
|
11
|
+
export { toOotleNetwork } from "./ootleNetwork";
|
|
12
|
+
export { OWNERSHIP_PROOF_CHALLENGE_MAX_LENGTH, buildOwnershipProofMessage, buildWalletOwnershipMessage } from "./ownershipProof";
|
|
13
|
+
export type { EncryptedVault } from "./vault";
|
|
14
|
+
export { decryptVault, encryptVault, fromHex, toHex } from "./vault";
|
|
15
|
+
export { withTimeout } from "./timeout";
|
|
16
|
+
export type { KeyValueStore, PendingShield, ShieldedOutputRecord } from "./storage";
|
|
17
|
+
export { addPendingShield, addShieldedOutput, configureOotleStorage, getKnownVersions, getPrivatePaymentScanCursor, listPendingShields, listShieldedOutputs, localAccountId, markShieldedOutputSpent, removePendingShield, serialized, setKnownVersions, setPrivatePaymentScanCursor, wipeOotleState, } from "./storage";
|
|
18
|
+
export { chromeStorageAdapter, inMemoryAdapter, localStorageAdapter } from "./adapters";
|
|
19
|
+
export type { PrivateBalance, TokenBalance } from "./wallet";
|
|
20
|
+
export { OotleAccount, assertValidMinimumValuePromise, buildHtlcSpendStatement, extractMissingSubstateAddress, extractStaleLockVersion, forgetKnownVersions, pollTransactionResult, recoverPendingShields, resetKnownVersions, resolveInputsWithRetry, resolveSendPrivatelyPlan, resolveUnshieldPlan, selectShieldedUtxosForAmount, selectUnspentShieldedOutputs, substateExists, summarizePrivateHoldings, synthesizeShieldedOnlyBalances, } from "./wallet";
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,YAAY,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAE7E,OAAO,EACL,wBAAwB,EACxB,iCAAiC,EACjC,6BAA6B,EAC7B,sBAAsB,GACvB,MAAM,oBAAoB,CAAC;AAE5B,YAAY,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAClF,OAAO,EAAE,+BAA+B,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAC;AAE7F,YAAY,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEjD,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,0BAA0B,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAE7H,YAAY,EAAE,oBAAoB,EAAE,MAAM,QAAQ,CAAC;AACnD,OAAO,EAAE,4BAA4B,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAEtE,YAAY,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEhD,OAAO,EAAE,oCAAoC,EAAE,0BAA0B,EAAE,2BAA2B,EAAE,MAAM,kBAAkB,CAAC;AAEjI,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErE,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExC,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AACpF,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,qBAAqB,EACrB,gBAAgB,EAChB,2BAA2B,EAC3B,kBAAkB,EAClB,mBAAmB,EACnB,cAAc,EACd,uBAAuB,EACvB,mBAAmB,EACnB,UAAU,EACV,gBAAgB,EAChB,2BAA2B,EAC3B,cAAc,GACf,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAExF,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7D,OAAO,EACL,YAAY,EACZ,8BAA8B,EAC9B,uBAAuB,EACvB,6BAA6B,EAC7B,uBAAuB,EACvB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,EAClB,sBAAsB,EACtB,wBAAwB,EACxB,mBAAmB,EACnB,4BAA4B,EAC5B,4BAA4B,EAC5B,cAAc,EACd,wBAAwB,EACxB,8BAA8B,GAC/B,MAAM,UAAU,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Public surface. Import from the package root ("@chironbuilder/ootle-sdk") rather than reaching
|
|
2
|
+
// into individual files -- the module layout underneath is free to change; this file is not.
|
|
3
|
+
export { ACCOUNT_TEMPLATE_ADDRESS, componentAddressFromWalletAddress, deriveAccountComponentAddress, deriveComponentAddress, } from "./componentAddress";
|
|
4
|
+
export { scanTransactionsForOwnedOutputs, sumConfidentialCommitments } from "./confidential";
|
|
5
|
+
export { deriveAccountKeys } from "./derivation";
|
|
6
|
+
export { DomainSeparatedHasher, KEY_MANAGER_DOMAIN, KEY_MANAGER_DOMAIN_VERSION, keyManagerDomainHasher } from "./domainHash";
|
|
7
|
+
export { accessRuleRequiringPublicKey, htlcConditions } from "./htlc";
|
|
8
|
+
export { toOotleNetwork } from "./ootleNetwork";
|
|
9
|
+
export { OWNERSHIP_PROOF_CHALLENGE_MAX_LENGTH, buildOwnershipProofMessage, buildWalletOwnershipMessage } from "./ownershipProof";
|
|
10
|
+
export { decryptVault, encryptVault, fromHex, toHex } from "./vault";
|
|
11
|
+
export { withTimeout } from "./timeout";
|
|
12
|
+
export { addPendingShield, addShieldedOutput, configureOotleStorage, getKnownVersions, getPrivatePaymentScanCursor, listPendingShields, listShieldedOutputs, localAccountId, markShieldedOutputSpent, removePendingShield, serialized, setKnownVersions, setPrivatePaymentScanCursor, wipeOotleState, } from "./storage";
|
|
13
|
+
export { chromeStorageAdapter, inMemoryAdapter, localStorageAdapter } from "./adapters";
|
|
14
|
+
export { OotleAccount, assertValidMinimumValuePromise, buildHtlcSpendStatement, extractMissingSubstateAddress, extractStaleLockVersion, forgetKnownVersions, pollTransactionResult, recoverPendingShields, resetKnownVersions, resolveInputsWithRetry, resolveSendPrivatelyPlan, resolveUnshieldPlan, selectShieldedUtxosForAmount, selectUnspentShieldedOutputs, substateExists, summarizePrivateHoldings, synthesizeShieldedOnlyBalances, } from "./wallet";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ootleNetwork.d.ts","sourceRoot":"","sources":["../src/ootleNetwork.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAE9C,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,MAAM,CAAC;AAE/C,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CASzD"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Network } from "@tari-project/ootle";
|
|
2
|
+
export function toOotleNetwork(name) {
|
|
3
|
+
switch (name) {
|
|
4
|
+
case "esmeralda":
|
|
5
|
+
return Network.Esmeralda;
|
|
6
|
+
case "igor":
|
|
7
|
+
return Network.Igor;
|
|
8
|
+
default:
|
|
9
|
+
throw new Error(`Unknown network: ${name}`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const OWNERSHIP_PROOF_CHALLENGE_MAX_LENGTH = 500;
|
|
2
|
+
export declare function buildOwnershipProofMessage(networkByte: number, resourceAddress: string, substateId: string, challenge: string): Uint8Array;
|
|
3
|
+
export declare function buildWalletOwnershipMessage(networkByte: number, walletAddress: string, challenge: string): Uint8Array;
|
|
4
|
+
//# sourceMappingURL=ownershipProof.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ownershipProof.d.ts","sourceRoot":"","sources":["../src/ownershipProof.ts"],"names":[],"mappings":"AAsBA,eAAO,MAAM,oCAAoC,MAAM,CAAC;AAExD,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,MAAM,EACnB,eAAe,EAAE,MAAM,EACvB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,GAChB,UAAU,CAUZ;AAaD,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,UAAU,CASrH"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// A dApp-facing proof that this account currently controls a specific stealth output's spend
|
|
2
|
+
// key, without spending it and without producing anything that could ever be replayed as a real
|
|
3
|
+
// transaction's authorization signature.
|
|
4
|
+
//
|
|
5
|
+
// The one-time secret this signs with (`c + k`, from `stealthDhSecret`) is exactly what
|
|
6
|
+
// `SecretKeyWallet.addStealthSignature` uses to authorize spending the same output -- so the
|
|
7
|
+
// message this signs must never be, or be derivable into, a real `hashUnsignedTransaction` output.
|
|
8
|
+
// Real transaction signing hashes under domain "com.tari.ootle.transaction"
|
|
9
|
+
// (tari-dan `crates/transaction/src/hashing.rs`); this hashes under a disjoint domain of its own,
|
|
10
|
+
// so the two message spaces cannot collide under the same collision-resistance assumption Tari's
|
|
11
|
+
// own protocol already relies on to keep ITS OWN domains apart (see domainHash.ts).
|
|
12
|
+
//
|
|
13
|
+
// Byte-identical across every consumer of this SDK -- a dApp must get the same signature semantics
|
|
14
|
+
// from any wallet built on it. Verified empirically against tari-crypto's own committed domain-tag
|
|
15
|
+
// test vector and round-tripped against the real `schnorrSign` WASM primitive; see
|
|
16
|
+
// ownershipProof.test.ts and tari-paylink-dapp's scripts/verify-schnorr-roundtrip.mjs, which this
|
|
17
|
+
// construction must stay byte-identical to.
|
|
18
|
+
import { utf8ToBytes } from "@noble/hashes/utils.js";
|
|
19
|
+
import { DomainSeparatedHasher } from "./domainHash";
|
|
20
|
+
const OWNERSHIP_PROOF_DOMAIN = "com.tari.paylink.ownership_proof";
|
|
21
|
+
const OWNERSHIP_PROOF_VERSION = 1;
|
|
22
|
+
export const OWNERSHIP_PROOF_CHALLENGE_MAX_LENGTH = 500;
|
|
23
|
+
export function buildOwnershipProofMessage(networkByte, resourceAddress, substateId, challenge) {
|
|
24
|
+
if (challenge.length > OWNERSHIP_PROOF_CHALLENGE_MAX_LENGTH) {
|
|
25
|
+
throw new Error(`Challenge text is too long (${OWNERSHIP_PROOF_CHALLENGE_MAX_LENGTH} characters max).`);
|
|
26
|
+
}
|
|
27
|
+
return new DomainSeparatedHasher(OWNERSHIP_PROOF_DOMAIN, OWNERSHIP_PROOF_VERSION, "v1", 64)
|
|
28
|
+
.chain(new Uint8Array([networkByte]))
|
|
29
|
+
.chain(utf8ToBytes(resourceAddress))
|
|
30
|
+
.chain(utf8ToBytes(substateId))
|
|
31
|
+
.chain(utf8ToBytes(challenge))
|
|
32
|
+
.finalize();
|
|
33
|
+
}
|
|
34
|
+
// A generic "I hold this otl_… wallet address" proof — signs with the account's own persistent
|
|
35
|
+
// owner key (the same key `SecretKeyWallet.signTransaction` uses for revealed-balance spends),
|
|
36
|
+
// NOT a per-output derived key. Same domain-separation reasoning as above applies with equal force
|
|
37
|
+
// here: this key signs real transactions too, so the message must live under its own domain,
|
|
38
|
+
// disjoint from "com.tari.ootle.transaction", or a signature could be replayable as spend
|
|
39
|
+
// authorization. Deliberately a DIFFERENT domain from the per-output proof above as well, even
|
|
40
|
+
// though the two never share a signing key -- one domain per distinct kind of claim being signed,
|
|
41
|
+
// so a signature can never be mistaken for the wrong kind of proof.
|
|
42
|
+
const WALLET_OWNERSHIP_PROOF_DOMAIN = "com.tari.paylink.wallet_ownership_proof";
|
|
43
|
+
const WALLET_OWNERSHIP_PROOF_VERSION = 1;
|
|
44
|
+
export function buildWalletOwnershipMessage(networkByte, walletAddress, challenge) {
|
|
45
|
+
if (challenge.length > OWNERSHIP_PROOF_CHALLENGE_MAX_LENGTH) {
|
|
46
|
+
throw new Error(`Challenge text is too long (${OWNERSHIP_PROOF_CHALLENGE_MAX_LENGTH} characters max).`);
|
|
47
|
+
}
|
|
48
|
+
return new DomainSeparatedHasher(WALLET_OWNERSHIP_PROOF_DOMAIN, WALLET_OWNERSHIP_PROOF_VERSION, "v1", 64)
|
|
49
|
+
.chain(new Uint8Array([networkByte]))
|
|
50
|
+
.chain(utf8ToBytes(walletAddress))
|
|
51
|
+
.chain(utf8ToBytes(challenge))
|
|
52
|
+
.finalize();
|
|
53
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface KeyValueStore {
|
|
2
|
+
get<T>(key: string): Promise<T | undefined>;
|
|
3
|
+
set<T>(key: string, value: T): Promise<void>;
|
|
4
|
+
remove(key: string): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
/** Call once, before any storage-touching `OotleAccount` method, with an adapter over whatever
|
|
7
|
+
* this host's real storage is (see `adapters.ts` for ready-made ones). */
|
|
8
|
+
export declare function configureOotleStorage(store: KeyValueStore): void;
|
|
9
|
+
export interface ShieldedOutputRecord {
|
|
10
|
+
accountId: string;
|
|
11
|
+
resourceAddress: string;
|
|
12
|
+
/** 32-byte Pedersen commitment, hex. */
|
|
13
|
+
commitment: string;
|
|
14
|
+
/** Raw, resource-native units (matches TokenBalance.amount's convention). */
|
|
15
|
+
amount: string;
|
|
16
|
+
transactionId: string;
|
|
17
|
+
createdAt: number;
|
|
18
|
+
/** Set true once this output has been spent via unshield. */
|
|
19
|
+
spent: boolean;
|
|
20
|
+
/** The output's plaintext memo, if any. Absent, not empty-string, when it carries none. */
|
|
21
|
+
memo?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A shield or unshield submitted but not yet recorded in `shieldedOutputs` — carries every field
|
|
25
|
+
* needed to complete the write on recovery, since a reload between finalization and the storage
|
|
26
|
+
* write would otherwise strand the only lead to a real commitment.
|
|
27
|
+
*/
|
|
28
|
+
export interface PendingShield {
|
|
29
|
+
transactionId: string;
|
|
30
|
+
accountId: string;
|
|
31
|
+
resourceAddress: string;
|
|
32
|
+
amount: string;
|
|
33
|
+
/** For an unshield or private send: the now-spent commitments to mark on recovery. */
|
|
34
|
+
spentCommitments?: string[];
|
|
35
|
+
/** This account's own new commitment, when the operation creates one. */
|
|
36
|
+
ownCommitment?: string;
|
|
37
|
+
/** This account's own memo for the operation, if any. */
|
|
38
|
+
memo?: string;
|
|
39
|
+
}
|
|
40
|
+
export declare function localAccountId(index: number): string;
|
|
41
|
+
/** Runs a whole read-modify-write cycle to completion before the next one starts, so two
|
|
42
|
+
* concurrent callers (two tabs, a page request racing a popup request) can't interleave their
|
|
43
|
+
* read-modify-write on this key and silently clobber each other's write. */
|
|
44
|
+
export declare function serialized<T>(fn: () => Promise<T>): Promise<T>;
|
|
45
|
+
export declare function listShieldedOutputs(accountId: string): Promise<ShieldedOutputRecord[]>;
|
|
46
|
+
export declare function addShieldedOutput(record: ShieldedOutputRecord): Promise<void>;
|
|
47
|
+
export declare function markShieldedOutputSpent(accountId: string, commitment: string): Promise<void>;
|
|
48
|
+
export declare function listPendingShields(): Promise<PendingShield[]>;
|
|
49
|
+
export declare function addPendingShield(pending: PendingShield): Promise<void>;
|
|
50
|
+
export declare function removePendingShield(transactionId: string): Promise<void>;
|
|
51
|
+
export declare function getPrivatePaymentScanCursor(accountId: string): Promise<string | null>;
|
|
52
|
+
export declare function setPrivatePaymentScanCursor(accountId: string, transactionId: string): Promise<void>;
|
|
53
|
+
/** Substate version cache -- see `wallet.ts`'s own doc comment on `knownVersionsPromise` for why
|
|
54
|
+
* this exists at all. */
|
|
55
|
+
export declare function getKnownVersions(): Promise<Record<string, number>>;
|
|
56
|
+
export declare function setKnownVersions(known: Record<string, number>): Promise<void>;
|
|
57
|
+
/** Clears every key this module owns -- call this from a host's own "erase wallet" flow. Does not
|
|
58
|
+
* touch anything else the host's `KeyValueStore` might hold (address books, settings, ...); this
|
|
59
|
+
* SDK never wrote those keys, so it isn't this function's place to remove them. */
|
|
60
|
+
export declare function wipeOotleState(): Promise<void>;
|
|
61
|
+
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IAC5C,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAID;0EAC0E;AAC1E,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAEhE;AAcD,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,KAAK,EAAE,OAAO,CAAC;IACf,2FAA2F;IAC3F,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,sFAAsF;IACtF,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,yEAAyE;IACzE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAiBD,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAaD;;4EAE4E;AAC5E,wBAAgB,UAAU,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAO9D;AAED,wBAAsB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAE5F;AAED,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAInF;AAED,wBAAsB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASlG;AAED,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CAEnE;AAED,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAM5E;AAED,wBAAsB,mBAAmB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAK9E;AAED,wBAAsB,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAE3F;AAED,wBAAsB,2BAA2B,CAAC,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKzG;AAED;yBACyB;AACzB,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAExE;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAInF;AAED;;mFAEmF;AACnF,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAIpD"}
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// The persistence surface `wallet.ts` (the account core) needs, backed by whatever key-value store
|
|
2
|
+
// the host application supplies -- a Chrome extension has `chrome.storage.local`, a web page has
|
|
3
|
+
// `localStorage`, a future host might have something else again. This module never touches a
|
|
4
|
+
// concrete storage API directly; it reads and writes through `KeyValueStore`, configured once via
|
|
5
|
+
// `configureOotleStorage()` before any `OotleAccount` method that touches storage is called.
|
|
6
|
+
//
|
|
7
|
+
// Deliberately narrow: this is exactly what the account core itself needs (shielded outputs,
|
|
8
|
+
// pending shields, the private-payment scan cursor, the known-substate-versions cache) -- not
|
|
9
|
+
// address books, connected-site permissions, daemon connections, or a full transaction-history UI.
|
|
10
|
+
// Those are application concerns with very different shapes across hosts, and belong in each
|
|
11
|
+
// consuming app's own storage layer, not in this SDK.
|
|
12
|
+
let configuredStore = null;
|
|
13
|
+
/** Call once, before any storage-touching `OotleAccount` method, with an adapter over whatever
|
|
14
|
+
* this host's real storage is (see `adapters.ts` for ready-made ones). */
|
|
15
|
+
export function configureOotleStorage(store) {
|
|
16
|
+
configuredStore = store;
|
|
17
|
+
}
|
|
18
|
+
function store() {
|
|
19
|
+
if (!configuredStore) {
|
|
20
|
+
throw new Error("Ootle SDK storage has not been configured. Call configureOotleStorage(store) once at startup " +
|
|
21
|
+
"with a KeyValueStore for this host (see adapters.ts).");
|
|
22
|
+
}
|
|
23
|
+
return configuredStore;
|
|
24
|
+
}
|
|
25
|
+
const STORAGE_KEY = "ootle-sdk/v1";
|
|
26
|
+
const DEFAULTS = {
|
|
27
|
+
shieldedOutputs: [],
|
|
28
|
+
pendingShields: [],
|
|
29
|
+
privatePaymentScanCursors: {},
|
|
30
|
+
knownVersions: {},
|
|
31
|
+
};
|
|
32
|
+
export function localAccountId(index) {
|
|
33
|
+
return `local:${index}`;
|
|
34
|
+
}
|
|
35
|
+
async function read() {
|
|
36
|
+
const raw = await store().get(STORAGE_KEY);
|
|
37
|
+
return { ...DEFAULTS, ...raw };
|
|
38
|
+
}
|
|
39
|
+
async function write(patch) {
|
|
40
|
+
await store().set(STORAGE_KEY, { ...(await read()), ...patch });
|
|
41
|
+
}
|
|
42
|
+
let writeQueue = Promise.resolve();
|
|
43
|
+
/** Runs a whole read-modify-write cycle to completion before the next one starts, so two
|
|
44
|
+
* concurrent callers (two tabs, a page request racing a popup request) can't interleave their
|
|
45
|
+
* read-modify-write on this key and silently clobber each other's write. */
|
|
46
|
+
export function serialized(fn) {
|
|
47
|
+
const result = writeQueue.then(fn, fn);
|
|
48
|
+
writeQueue = result.then(() => undefined, () => undefined);
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
export async function listShieldedOutputs(accountId) {
|
|
52
|
+
return (await read()).shieldedOutputs.filter((r) => r.accountId === accountId);
|
|
53
|
+
}
|
|
54
|
+
export async function addShieldedOutput(record) {
|
|
55
|
+
await serialized(async () => {
|
|
56
|
+
write({ shieldedOutputs: [...(await read()).shieldedOutputs, record] });
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export async function markShieldedOutputSpent(accountId, commitment) {
|
|
60
|
+
await serialized(async () => {
|
|
61
|
+
const state = await read();
|
|
62
|
+
await write({
|
|
63
|
+
shieldedOutputs: state.shieldedOutputs.map((r) => r.accountId === accountId && r.commitment === commitment ? { ...r, spent: true } : r),
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export async function listPendingShields() {
|
|
68
|
+
return (await read()).pendingShields;
|
|
69
|
+
}
|
|
70
|
+
export async function addPendingShield(pending) {
|
|
71
|
+
await serialized(async () => {
|
|
72
|
+
const state = await read();
|
|
73
|
+
if (state.pendingShields.some((p) => p.transactionId === pending.transactionId))
|
|
74
|
+
return;
|
|
75
|
+
await write({ pendingShields: [...state.pendingShields, pending] });
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export async function removePendingShield(transactionId) {
|
|
79
|
+
await serialized(async () => {
|
|
80
|
+
const state = await read();
|
|
81
|
+
await write({ pendingShields: state.pendingShields.filter((p) => p.transactionId !== transactionId) });
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
export async function getPrivatePaymentScanCursor(accountId) {
|
|
85
|
+
return (await read()).privatePaymentScanCursors[accountId] ?? null;
|
|
86
|
+
}
|
|
87
|
+
export async function setPrivatePaymentScanCursor(accountId, transactionId) {
|
|
88
|
+
await serialized(async () => {
|
|
89
|
+
const state = await read();
|
|
90
|
+
await write({ privatePaymentScanCursors: { ...state.privatePaymentScanCursors, [accountId]: transactionId } });
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/** Substate version cache -- see `wallet.ts`'s own doc comment on `knownVersionsPromise` for why
|
|
94
|
+
* this exists at all. */
|
|
95
|
+
export async function getKnownVersions() {
|
|
96
|
+
return (await read()).knownVersions;
|
|
97
|
+
}
|
|
98
|
+
export async function setKnownVersions(known) {
|
|
99
|
+
await serialized(async () => {
|
|
100
|
+
await write({ knownVersions: known });
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/** Clears every key this module owns -- call this from a host's own "erase wallet" flow. Does not
|
|
104
|
+
* touch anything else the host's `KeyValueStore` might hold (address books, settings, ...); this
|
|
105
|
+
* SDK never wrote those keys, so it isn't this function's place to remove them. */
|
|
106
|
+
export async function wipeOotleState() {
|
|
107
|
+
await serialized(async () => {
|
|
108
|
+
await store().remove(STORAGE_KEY);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeout.d.ts","sourceRoot":"","sources":["../src/timeout.ts"],"names":[],"mappings":"AAOA,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAczF"}
|
package/dist/timeout.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// A hung fetch (or any other network stall) inside a page-triggered request otherwise blocks that
|
|
2
|
+
// page's promise forever with no error and no way to recover short of reloading the extension —
|
|
3
|
+
// confirmed empirically hitting exactly this waiting on a testnet indexer response, and again
|
|
4
|
+
// waiting on an unreachable wallet daemon (a plain `fetch()` to a port nothing is listening on
|
|
5
|
+
// doesn't reject quickly on every platform). Every individual network step in OotleAccount's and
|
|
6
|
+
// DaemonAccount's execute() is wrapped in this so a stall surfaces as a clear timeout error
|
|
7
|
+
// instead of an indefinite hang.
|
|
8
|
+
export function withTimeout(promise, ms, label) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const timer = setTimeout(() => reject(new Error(`Timed out after ${ms}ms while ${label}.`)), ms);
|
|
11
|
+
promise.then((v) => {
|
|
12
|
+
clearTimeout(timer);
|
|
13
|
+
resolve(v);
|
|
14
|
+
}, (e) => {
|
|
15
|
+
clearTimeout(timer);
|
|
16
|
+
reject(e);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
package/dist/vault.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface EncryptedVault {
|
|
2
|
+
version: 1;
|
|
3
|
+
salt: string;
|
|
4
|
+
iv: string;
|
|
5
|
+
ciphertext: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function toHex(bytes: Uint8Array): string;
|
|
8
|
+
export declare function fromHex(hex: string): Uint8Array;
|
|
9
|
+
export declare function encryptVault(password: string, plaintext: Uint8Array): Promise<EncryptedVault>;
|
|
10
|
+
/** @throws if the password is wrong or the vault is corrupt. */
|
|
11
|
+
export declare function decryptVault(password: string, vault: EncryptedVault): Promise<Uint8Array>;
|
|
12
|
+
//# sourceMappingURL=vault.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vault.d.ts","sourceRoot":"","sources":["../src/vault.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,CAAC,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAI/C;AAED,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAI/C;AAeD,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,GAAG,OAAO,CAAC,cAAc,CAAC,CAQnG;AAED,gEAAgE;AAChE,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,CAc/F"}
|
package/dist/vault.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Password-based encryption for the wallet's master seed, using the browser's native WebCrypto
|
|
2
|
+
// (available in both the background service worker and the popup) — no custom crypto here.
|
|
3
|
+
// PBKDF2-HMAC-SHA256 with 600,000 iterations (OWASP's 2023 minimum recommendation) derives an
|
|
4
|
+
// AES-256-GCM key from the unlock password; the encrypted blob is what's persisted to
|
|
5
|
+
// chrome.storage.local. The derived key and plaintext seed only ever live in memory, and only for
|
|
6
|
+
// as long as the wallet is unlocked (see background/session.ts).
|
|
7
|
+
const PBKDF2_ITERATIONS = 600_000;
|
|
8
|
+
export function toHex(bytes) {
|
|
9
|
+
return Array.from(bytes)
|
|
10
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
11
|
+
.join("");
|
|
12
|
+
}
|
|
13
|
+
export function fromHex(hex) {
|
|
14
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
15
|
+
for (let i = 0; i < bytes.length; i++)
|
|
16
|
+
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
17
|
+
return bytes;
|
|
18
|
+
}
|
|
19
|
+
async function deriveAesKey(password, salt) {
|
|
20
|
+
const keyMaterial = await crypto.subtle.importKey("raw", new TextEncoder().encode(password), "PBKDF2", false, [
|
|
21
|
+
"deriveKey",
|
|
22
|
+
]);
|
|
23
|
+
return crypto.subtle.deriveKey({ name: "PBKDF2", salt: salt, iterations: PBKDF2_ITERATIONS, hash: "SHA-256" }, keyMaterial, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);
|
|
24
|
+
}
|
|
25
|
+
export async function encryptVault(password, plaintext) {
|
|
26
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
27
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
28
|
+
const key = await deriveAesKey(password, salt);
|
|
29
|
+
const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: iv }, key, plaintext));
|
|
30
|
+
return { version: 1, salt: toHex(salt), iv: toHex(iv), ciphertext: toHex(ciphertext) };
|
|
31
|
+
}
|
|
32
|
+
/** @throws if the password is wrong or the vault is corrupt. */
|
|
33
|
+
export async function decryptVault(password, vault) {
|
|
34
|
+
const salt = fromHex(vault.salt);
|
|
35
|
+
const iv = fromHex(vault.iv);
|
|
36
|
+
const key = await deriveAesKey(password, salt);
|
|
37
|
+
try {
|
|
38
|
+
const plaintext = await crypto.subtle.decrypt({ name: "AES-GCM", iv: iv }, key, fromHex(vault.ciphertext));
|
|
39
|
+
return new Uint8Array(plaintext);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new Error("Incorrect password");
|
|
43
|
+
}
|
|
44
|
+
}
|