@totemsdk/omnia-vtxo 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/anchor.d.ts +90 -0
- package/dist/anchor.js +112 -0
- package/dist/cjs/anchor.d.ts +90 -0
- package/dist/cjs/anchor.js +119 -0
- package/dist/cjs/commitment.d.ts +41 -0
- package/dist/cjs/commitment.js +173 -0
- package/dist/cjs/constants.d.ts +10 -0
- package/dist/cjs/constants.js +13 -0
- package/dist/cjs/durable-store.d.ts +33 -0
- package/dist/cjs/durable-store.js +93 -0
- package/dist/cjs/errors.d.ts +33 -0
- package/dist/cjs/errors.js +77 -0
- package/dist/cjs/exit-tx.d.ts +57 -0
- package/dist/cjs/exit-tx.js +83 -0
- package/dist/cjs/exit.d.ts +29 -0
- package/dist/cjs/exit.js +115 -0
- package/dist/cjs/index.d.ts +21 -0
- package/dist/cjs/index.js +37 -0
- package/dist/cjs/memory-store.d.ts +17 -0
- package/dist/cjs/memory-store.js +51 -0
- package/dist/cjs/merge.d.ts +8 -0
- package/dist/cjs/merge.js +106 -0
- package/dist/cjs/operator.d.ts +29 -0
- package/dist/cjs/operator.js +65 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/policy.d.ts +22 -0
- package/dist/cjs/policy.js +27 -0
- package/dist/cjs/pool-script.d.ts +36 -0
- package/dist/cjs/pool-script.js +60 -0
- package/dist/cjs/pool.d.ts +5 -0
- package/dist/cjs/pool.js +60 -0
- package/dist/cjs/receipt.d.ts +31 -0
- package/dist/cjs/receipt.js +52 -0
- package/dist/cjs/refresh.d.ts +8 -0
- package/dist/cjs/refresh.js +84 -0
- package/dist/cjs/serialization.d.ts +5 -0
- package/dist/cjs/serialization.js +31 -0
- package/dist/cjs/split.d.ts +8 -0
- package/dist/cjs/split.js +96 -0
- package/dist/cjs/transfer.d.ts +8 -0
- package/dist/cjs/transfer.js +152 -0
- package/dist/cjs/types.d.ts +166 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/verify.d.ts +23 -0
- package/dist/cjs/verify.js +121 -0
- package/dist/cjs/vtxo.d.ts +14 -0
- package/dist/cjs/vtxo.js +96 -0
- package/dist/commitment.d.ts +8 -0
- package/dist/commitment.js +38 -36
- package/dist/constants.d.ts +0 -2
- package/dist/constants.js +4 -9
- package/dist/durable-store.js +8 -11
- package/dist/errors.js +10 -23
- package/dist/exit-tx.d.ts +57 -0
- package/dist/exit-tx.js +76 -0
- package/dist/exit.d.ts +4 -1
- package/dist/exit.js +20 -26
- package/dist/index.d.ts +5 -0
- package/dist/index.js +21 -32
- package/dist/memory-store.js +3 -7
- package/dist/merge.js +15 -19
- package/dist/operator.d.ts +29 -0
- package/dist/operator.js +61 -0
- package/dist/policy.d.ts +1 -1
- package/dist/policy.js +3 -7
- package/dist/pool-script.d.ts +36 -0
- package/dist/pool-script.js +55 -0
- package/dist/pool.js +21 -27
- package/dist/receipt.d.ts +31 -0
- package/dist/receipt.js +48 -0
- package/dist/refresh.js +10 -14
- package/dist/serialization.js +4 -10
- package/dist/split.js +13 -17
- package/dist/transfer.js +13 -17
- package/dist/types.d.ts +3 -1
- package/dist/types.js +1 -2
- package/dist/verify.js +7 -13
- package/dist/vtxo.js +13 -19
- package/package.json +18 -8
- package/rust/pkg/omnia_vtxo_wasm_bg.wasm +0 -0
- package/rust/pkg/package.json +1 -1
- package/rust/pkg-node/omnia_vtxo_wasm_bg.wasm +0 -0
- package/rust/pkg-node/package.json +1 -1
package/dist/memory-store.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.MemoryOmniaVtxoStore = void 0;
|
|
4
|
-
const errors_js_1 = require("./errors.js");
|
|
5
|
-
class MemoryOmniaVtxoStore {
|
|
1
|
+
import { VtxoStatusError } from './errors.js';
|
|
2
|
+
export class MemoryOmniaVtxoStore {
|
|
6
3
|
constructor() {
|
|
7
4
|
this.pools = new Map();
|
|
8
5
|
this.vtxos = new Map();
|
|
@@ -36,7 +33,7 @@ class MemoryOmniaVtxoStore {
|
|
|
36
33
|
if (!vtxo)
|
|
37
34
|
throw new Error(`VTXO ${vtxoId} not found`);
|
|
38
35
|
if (vtxo.status !== 'active') {
|
|
39
|
-
throw new
|
|
36
|
+
throw new VtxoStatusError(`Cannot mark VTXO ${vtxoId} as spent: status is '${vtxo.status}'`, vtxo.status);
|
|
40
37
|
}
|
|
41
38
|
// NOTE: Date.now() fallback is intentional and documented above.
|
|
42
39
|
const ts = now !== undefined ? now : Date.now();
|
|
@@ -48,4 +45,3 @@ class MemoryOmniaVtxoStore {
|
|
|
48
45
|
});
|
|
49
46
|
}
|
|
50
47
|
}
|
|
51
|
-
exports.MemoryOmniaVtxoStore = MemoryOmniaVtxoStore;
|
package/dist/merge.js
CHANGED
|
@@ -1,26 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.mergeVtxos = mergeVtxos;
|
|
4
|
-
const errors_js_1 = require("./errors.js");
|
|
5
|
-
const commitment_js_1 = require("./commitment.js");
|
|
6
|
-
const constants_js_1 = require("./constants.js");
|
|
1
|
+
import { VtxoStatusError, VtxoMergeError, VtxoOwnershipError } from './errors.js';
|
|
2
|
+
import { computeVtxoId, buildVtxoProofSet, computeReceiptId } from './commitment.js';
|
|
7
3
|
/**
|
|
8
4
|
* Merges multiple VTXOs into one.
|
|
9
5
|
* @param now - Timestamp in ms. Defaults to Date.now() if omitted — pass explicitly for determinism.
|
|
10
6
|
*/
|
|
11
|
-
function mergeVtxos(vtxos, params, now) {
|
|
7
|
+
export function mergeVtxos(vtxos, params, now) {
|
|
12
8
|
const ts = now !== undefined ? now : Date.now();
|
|
13
9
|
if (vtxos.length < 2) {
|
|
14
|
-
throw new
|
|
10
|
+
throw new VtxoMergeError('Merge requires at least 2 VTXOs');
|
|
15
11
|
}
|
|
16
12
|
const ids = vtxos.map(v => v.vtxoId);
|
|
17
13
|
const uniqueIds = new Set(ids);
|
|
18
14
|
if (uniqueIds.size !== ids.length) {
|
|
19
|
-
throw new
|
|
15
|
+
throw new VtxoMergeError('Duplicate VTXO IDs detected in merge — double-counting prevented');
|
|
20
16
|
}
|
|
21
17
|
for (const vtxo of vtxos) {
|
|
22
18
|
if (vtxo.status !== 'active') {
|
|
23
|
-
throw new
|
|
19
|
+
throw new VtxoStatusError(`Cannot merge VTXO ${vtxo.vtxoId}: status is '${vtxo.status}'`, vtxo.status);
|
|
24
20
|
}
|
|
25
21
|
}
|
|
26
22
|
const poolId = vtxos[0].poolId;
|
|
@@ -28,20 +24,20 @@ function mergeVtxos(vtxos, params, now) {
|
|
|
28
24
|
const inputOwner = vtxos[0].owner;
|
|
29
25
|
for (const vtxo of vtxos) {
|
|
30
26
|
if (vtxo.poolId !== poolId) {
|
|
31
|
-
throw new
|
|
27
|
+
throw new VtxoMergeError(`Cannot merge VTXOs from different pools: ${vtxo.poolId} vs ${poolId}`);
|
|
32
28
|
}
|
|
33
29
|
if (vtxo.tokenId !== tokenId) {
|
|
34
|
-
throw new
|
|
30
|
+
throw new VtxoMergeError(`Cannot merge VTXOs with different tokenIds: ${vtxo.tokenId} vs ${tokenId}`);
|
|
35
31
|
}
|
|
36
32
|
if (vtxo.owner !== inputOwner) {
|
|
37
|
-
throw new
|
|
33
|
+
throw new VtxoOwnershipError(`Cannot merge VTXOs with different owners: ${vtxo.owner} vs ${inputOwner}`);
|
|
38
34
|
}
|
|
39
35
|
}
|
|
40
36
|
if (params.owner !== inputOwner) {
|
|
41
|
-
throw new
|
|
37
|
+
throw new VtxoOwnershipError(`Merge output owner '${params.owner}' must match input owner '${inputOwner}'`);
|
|
42
38
|
}
|
|
43
39
|
const totalAmount = vtxos.reduce((sum, v) => sum + v.amount, BigInt(0));
|
|
44
|
-
const outputId =
|
|
40
|
+
const outputId = computeVtxoId({
|
|
45
41
|
poolId,
|
|
46
42
|
owner: params.owner,
|
|
47
43
|
amount: totalAmount,
|
|
@@ -75,7 +71,7 @@ function mergeVtxos(vtxos, params, now) {
|
|
|
75
71
|
siblings: [],
|
|
76
72
|
positions: [],
|
|
77
73
|
epoch: vtxos[0].epoch,
|
|
78
|
-
batchId:
|
|
74
|
+
batchId: '',
|
|
79
75
|
},
|
|
80
76
|
history: [
|
|
81
77
|
{
|
|
@@ -87,9 +83,9 @@ function mergeVtxos(vtxos, params, now) {
|
|
|
87
83
|
createdAt: ts,
|
|
88
84
|
updatedAt: ts,
|
|
89
85
|
};
|
|
90
|
-
const { proofs } =
|
|
86
|
+
const { proofs } = buildVtxoProofSet([rawOutput]);
|
|
91
87
|
const output = { ...rawOutput, proof: proofs[outputId] };
|
|
92
|
-
const receiptId =
|
|
88
|
+
const receiptId = computeReceiptId(poolId, 'merge', ids, [outputId], ts);
|
|
93
89
|
return {
|
|
94
90
|
inputs: inputVtxos,
|
|
95
91
|
output,
|
|
@@ -101,7 +97,7 @@ function mergeVtxos(vtxos, params, now) {
|
|
|
101
97
|
outputIds: [outputId],
|
|
102
98
|
epoch: vtxos[0].epoch,
|
|
103
99
|
at: ts,
|
|
104
|
-
signature:
|
|
100
|
+
signature: '',
|
|
105
101
|
},
|
|
106
102
|
};
|
|
107
103
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VTXO operator co-signature (RFC-038 V0).
|
|
3
|
+
*
|
|
4
|
+
* The operator signs the canonical batch commitment with a unified WOTS
|
|
5
|
+
* `TreeKey` (RFC-037), allocating one leaf per signature under a WOTS lease
|
|
6
|
+
* (RFC-013). This retires the mock operator-signature placeholder: a receipt's
|
|
7
|
+
* `signature` is now a real serialized `TreeSignature` over the commitment, and
|
|
8
|
+
* `verify` checks it against the operator's tree-root public key.
|
|
9
|
+
*
|
|
10
|
+
* The operator's identity is the TreeKey root public key, so a holder can verify
|
|
11
|
+
* a receipt without trusting transport identity.
|
|
12
|
+
*/
|
|
13
|
+
import { type TreeKey } from '@totemsdk/core';
|
|
14
|
+
import type { WotsLeaseProvider } from '@totemsdk/wots-lease';
|
|
15
|
+
import type { OmniaVtxoOperator } from './types.js';
|
|
16
|
+
/** Lease treeId for a pool's operator signatures. */
|
|
17
|
+
export declare function vtxoOperatorTreeId(poolId: string): string;
|
|
18
|
+
export interface VtxoOperatorOptions {
|
|
19
|
+
poolId: string;
|
|
20
|
+
/** The operator's unified WOTS TreeKey (RFC-037). */
|
|
21
|
+
treeKey: TreeKey;
|
|
22
|
+
/** Lease provider for one-time leaf allocation (RFC-013). */
|
|
23
|
+
leaseProvider: WotsLeaseProvider;
|
|
24
|
+
}
|
|
25
|
+
export interface VtxoOperatorWithIdentity extends OmniaVtxoOperator {
|
|
26
|
+
/** The operator's tree-root public key (its identity). */
|
|
27
|
+
readonly publicKey: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function createVtxoOperator(options: VtxoOperatorOptions): VtxoOperatorWithIdentity;
|
package/dist/operator.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VTXO operator co-signature (RFC-038 V0).
|
|
3
|
+
*
|
|
4
|
+
* The operator signs the canonical batch commitment with a unified WOTS
|
|
5
|
+
* `TreeKey` (RFC-037), allocating one leaf per signature under a WOTS lease
|
|
6
|
+
* (RFC-013). This retires the mock operator-signature placeholder: a receipt's
|
|
7
|
+
* `signature` is now a real serialized `TreeSignature` over the commitment, and
|
|
8
|
+
* `verify` checks it against the operator's tree-root public key.
|
|
9
|
+
*
|
|
10
|
+
* The operator's identity is the TreeKey root public key, so a holder can verify
|
|
11
|
+
* a receipt without trusting transport identity.
|
|
12
|
+
*/
|
|
13
|
+
import { bytesToHex, deserializeTreeSignature, hexToBytes, serializeTreeSignature, sha3_256, verifyTreeSignature, } from '@totemsdk/core';
|
|
14
|
+
/** Keys per tree level — the flat leaf index is `l1 * KEYS_PER_LEVEL + l2`. */
|
|
15
|
+
const KEYS_PER_LEVEL = 64;
|
|
16
|
+
/** Lease treeId for a pool's operator signatures. */
|
|
17
|
+
export function vtxoOperatorTreeId(poolId) {
|
|
18
|
+
return `omnia-vtxo:${poolId}`;
|
|
19
|
+
}
|
|
20
|
+
export function createVtxoOperator(options) {
|
|
21
|
+
const { poolId, treeKey, leaseProvider } = options;
|
|
22
|
+
const rootPubkey = treeKey.getPublicKey();
|
|
23
|
+
// The TreeKey carries mutable state (setUses + sign); serialize signing so
|
|
24
|
+
// concurrent co-signs cannot race on a shared key.
|
|
25
|
+
let signLock = Promise.resolve();
|
|
26
|
+
const doSign = async (data) => {
|
|
27
|
+
const reservation = await leaseProvider.reserveKeyUse({
|
|
28
|
+
treeId: vtxoOperatorTreeId(poolId),
|
|
29
|
+
purpose: 'vtxo-operator',
|
|
30
|
+
payloadHash: bytesToHex(sha3_256(data)),
|
|
31
|
+
});
|
|
32
|
+
const flat = reservation.indices.l1 * KEYS_PER_LEVEL + reservation.indices.l2;
|
|
33
|
+
try {
|
|
34
|
+
treeKey.setUses(flat);
|
|
35
|
+
const signature = serializeTreeSignature(treeKey.sign(data));
|
|
36
|
+
await leaseProvider.commitKeyUse(reservation.reservationId, 'vtxo-operator');
|
|
37
|
+
return '0x' + bytesToHex(signature);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
await leaseProvider.burnReservation(reservation.reservationId, 'sign-failed').catch(() => { });
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
poolId,
|
|
46
|
+
publicKey: '0x' + bytesToHex(rootPubkey).toLowerCase(),
|
|
47
|
+
sign(data) {
|
|
48
|
+
const run = signLock.then(() => doSign(data), () => doSign(data));
|
|
49
|
+
signLock = run.then(() => undefined, () => undefined);
|
|
50
|
+
return run;
|
|
51
|
+
},
|
|
52
|
+
async verify(data, sig) {
|
|
53
|
+
try {
|
|
54
|
+
return verifyTreeSignature(rootPubkey, data, deserializeTreeSignature(hexToBytes(sig)));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
package/dist/policy.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface VtxoExitIntent {
|
|
|
16
16
|
owner: string;
|
|
17
17
|
amount: bigint;
|
|
18
18
|
tokenId: string;
|
|
19
|
-
draftType: '
|
|
19
|
+
draftType: 'vtxo-exit';
|
|
20
20
|
}
|
|
21
21
|
export declare function buildVtxoTransferIntent(vtxo: OmniaVtxo, recipient: string, amount: bigint, nonce: string, changeNonce?: string): VtxoTransferIntent;
|
|
22
22
|
export declare function buildVtxoExitIntent(vtxo: OmniaVtxo): VtxoExitIntent;
|
package/dist/policy.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.buildVtxoTransferIntent = buildVtxoTransferIntent;
|
|
4
|
-
exports.buildVtxoExitIntent = buildVtxoExitIntent;
|
|
5
|
-
function buildVtxoTransferIntent(vtxo, recipient, amount, nonce, changeNonce) {
|
|
1
|
+
export function buildVtxoTransferIntent(vtxo, recipient, amount, nonce, changeNonce) {
|
|
6
2
|
return {
|
|
7
3
|
type: 'vtxo-transfer',
|
|
8
4
|
poolId: vtxo.poolId,
|
|
@@ -14,7 +10,7 @@ function buildVtxoTransferIntent(vtxo, recipient, amount, nonce, changeNonce) {
|
|
|
14
10
|
changeNonce,
|
|
15
11
|
};
|
|
16
12
|
}
|
|
17
|
-
function buildVtxoExitIntent(vtxo) {
|
|
13
|
+
export function buildVtxoExitIntent(vtxo) {
|
|
18
14
|
return {
|
|
19
15
|
type: 'vtxo-exit',
|
|
20
16
|
poolId: vtxo.poolId,
|
|
@@ -22,6 +18,6 @@ function buildVtxoExitIntent(vtxo) {
|
|
|
22
18
|
owner: vtxo.owner,
|
|
23
19
|
amount: vtxo.amount,
|
|
24
20
|
tokenId: vtxo.tokenId,
|
|
25
|
-
draftType: '
|
|
21
|
+
draftType: 'vtxo-exit',
|
|
26
22
|
};
|
|
27
23
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VTXO pool on-chain script (RFC-038 V2 — non-custodial).
|
|
3
|
+
*
|
|
4
|
+
* The pool is an on-chain state output holding `totalCapacity`. An epoch's batch
|
|
5
|
+
* root is committed as STATE(100), the epoch number in STATE(101), and the
|
|
6
|
+
* owner's absolute exit-eligibility block in STATE(102).
|
|
7
|
+
*
|
|
8
|
+
* Custody (RFC-044 / CUSTODY-POLICY §2):
|
|
9
|
+
* - **Unilateral owner exit.** After `@BLOCK GT EXITBLOCK` the owner alone may
|
|
10
|
+
* spend (`RETURN SIGNEDBY(OWNER)`) — no operator signature or liveness. This
|
|
11
|
+
* is exercised by the pre-signed exit tx (`exit-tx.ts`).
|
|
12
|
+
* - **Cooperative update.** Before the exit window the pool may only move by
|
|
13
|
+
* `MULTISIG(2 OPERATOR OWNER)`, epoch-monotone (`ASSERT EPOCH GT
|
|
14
|
+
* PREVSTATE(101)`), with the exit block immutable (`ASSERT SAMESTATE(102
|
|
15
|
+
* 102)`) so the operator can never push the owner's escape clock, and the
|
|
16
|
+
* value confined to the same script address (`ASSERT VERIFYOUT(@INPUT
|
|
17
|
+
* @ADDRESS @AMOUNT @TOKENID TRUE)`) so the operator can never pay itself.
|
|
18
|
+
*
|
|
19
|
+
* The operator holds no unilateral path: it cannot move funds, cannot advance
|
|
20
|
+
* the exit block, and cannot stop the owner's exit. It is a liveness-only peer.
|
|
21
|
+
*/
|
|
22
|
+
export interface PoolScriptParams {
|
|
23
|
+
/** Operator WOTS public-key digest (64 hex). */
|
|
24
|
+
operatorPublicKeyDigest: string;
|
|
25
|
+
/** Pool owner WOTS public-key digest (64 hex). */
|
|
26
|
+
ownerPublicKeyDigest: string;
|
|
27
|
+
/** Absolute block height after which the owner may exit unilaterally. */
|
|
28
|
+
exitBlock: number;
|
|
29
|
+
}
|
|
30
|
+
/** Non-custodial, epoch-monotone, owner-escapable state-carrying pool script. */
|
|
31
|
+
export declare function buildPoolScript(params: PoolScriptParams): string;
|
|
32
|
+
export declare function poolScriptAddress(script: string): string;
|
|
33
|
+
export declare function buildAndHashPoolScript(params: PoolScriptParams): {
|
|
34
|
+
script: string;
|
|
35
|
+
address: string;
|
|
36
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VTXO pool on-chain script (RFC-038 V2 — non-custodial).
|
|
3
|
+
*
|
|
4
|
+
* The pool is an on-chain state output holding `totalCapacity`. An epoch's batch
|
|
5
|
+
* root is committed as STATE(100), the epoch number in STATE(101), and the
|
|
6
|
+
* owner's absolute exit-eligibility block in STATE(102).
|
|
7
|
+
*
|
|
8
|
+
* Custody (RFC-044 / CUSTODY-POLICY §2):
|
|
9
|
+
* - **Unilateral owner exit.** After `@BLOCK GT EXITBLOCK` the owner alone may
|
|
10
|
+
* spend (`RETURN SIGNEDBY(OWNER)`) — no operator signature or liveness. This
|
|
11
|
+
* is exercised by the pre-signed exit tx (`exit-tx.ts`).
|
|
12
|
+
* - **Cooperative update.** Before the exit window the pool may only move by
|
|
13
|
+
* `MULTISIG(2 OPERATOR OWNER)`, epoch-monotone (`ASSERT EPOCH GT
|
|
14
|
+
* PREVSTATE(101)`), with the exit block immutable (`ASSERT SAMESTATE(102
|
|
15
|
+
* 102)`) so the operator can never push the owner's escape clock, and the
|
|
16
|
+
* value confined to the same script address (`ASSERT VERIFYOUT(@INPUT
|
|
17
|
+
* @ADDRESS @AMOUNT @TOKENID TRUE)`) so the operator can never pay itself.
|
|
18
|
+
*
|
|
19
|
+
* The operator holds no unilateral path: it cannot move funds, cannot advance
|
|
20
|
+
* the exit block, and cannot stop the owner's exit. It is a liveness-only peer.
|
|
21
|
+
*/
|
|
22
|
+
import { computeScriptAddress } from '@totemsdk/core';
|
|
23
|
+
function pkHex(digest, label) {
|
|
24
|
+
const raw = digest.replace(/^0x/i, '');
|
|
25
|
+
if (!/^[0-9a-fA-F]{64}$/.test(raw)) {
|
|
26
|
+
throw new Error(`${label} public key digest must be 64 hex chars, got ${JSON.stringify(digest)}`);
|
|
27
|
+
}
|
|
28
|
+
return '0X' + raw.toUpperCase();
|
|
29
|
+
}
|
|
30
|
+
/** Non-custodial, epoch-monotone, owner-escapable state-carrying pool script. */
|
|
31
|
+
export function buildPoolScript(params) {
|
|
32
|
+
const owner = pkHex(params.ownerPublicKeyDigest, 'owner');
|
|
33
|
+
const operator = pkHex(params.operatorPublicKeyDigest, 'operator');
|
|
34
|
+
if (!Number.isInteger(params.exitBlock) || params.exitBlock < 0) {
|
|
35
|
+
throw new Error(`exitBlock must be a non-negative integer, got ${JSON.stringify(params.exitBlock)}`);
|
|
36
|
+
}
|
|
37
|
+
return [
|
|
38
|
+
'LET EPOCH=STATE(101)',
|
|
39
|
+
'LET EXITBLOCK=STATE(102)',
|
|
40
|
+
'IF @BLOCK GT EXITBLOCK THEN',
|
|
41
|
+
` RETURN SIGNEDBY(${owner})`,
|
|
42
|
+
'ENDIF',
|
|
43
|
+
'ASSERT EPOCH GT PREVSTATE(101)',
|
|
44
|
+
'ASSERT SAMESTATE(102 102)',
|
|
45
|
+
'ASSERT VERIFYOUT(@INPUT @ADDRESS @AMOUNT @TOKENID TRUE)',
|
|
46
|
+
`RETURN MULTISIG(2 ${operator} ${owner})`,
|
|
47
|
+
].join('\n');
|
|
48
|
+
}
|
|
49
|
+
export function poolScriptAddress(script) {
|
|
50
|
+
return computeScriptAddress(script);
|
|
51
|
+
}
|
|
52
|
+
export function buildAndHashPoolScript(params) {
|
|
53
|
+
const script = buildPoolScript(params);
|
|
54
|
+
return { script, address: poolScriptAddress(script) };
|
|
55
|
+
}
|
package/dist/pool.js
CHANGED
|
@@ -1,24 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
exports.updatePoolRoot = updatePoolRoot;
|
|
6
|
-
exports.advancePoolEpoch = advancePoolEpoch;
|
|
7
|
-
const errors_js_1 = require("./errors.js");
|
|
8
|
-
const constants_js_1 = require("./constants.js");
|
|
9
|
-
const commitment_js_1 = require("./commitment.js");
|
|
10
|
-
function createPool(params, now) {
|
|
1
|
+
import { VtxoPoolCapacityError, VtxoPolicyError } from './errors.js';
|
|
2
|
+
import { DEFAULT_POLICY, EMPTY_TREE_ROOT, EPOCH_ZERO } from './constants.js';
|
|
3
|
+
import { computePoolId } from './commitment.js';
|
|
4
|
+
export function createPool(params, now) {
|
|
11
5
|
const policy = {
|
|
12
|
-
minAmount: params.policy?.minAmount ??
|
|
13
|
-
maxAmount: params.policy?.maxAmount ??
|
|
14
|
-
maxMergeInputs: params.policy?.maxMergeInputs ??
|
|
15
|
-
maxSplitOutputs: params.policy?.maxSplitOutputs ??
|
|
16
|
-
exitTimelockSeconds: params.policy?.exitTimelockSeconds ??
|
|
6
|
+
minAmount: params.policy?.minAmount ?? DEFAULT_POLICY.minAmount,
|
|
7
|
+
maxAmount: params.policy?.maxAmount ?? DEFAULT_POLICY.maxAmount,
|
|
8
|
+
maxMergeInputs: params.policy?.maxMergeInputs ?? DEFAULT_POLICY.maxMergeInputs,
|
|
9
|
+
maxSplitOutputs: params.policy?.maxSplitOutputs ?? DEFAULT_POLICY.maxSplitOutputs,
|
|
10
|
+
exitTimelockSeconds: params.policy?.exitTimelockSeconds ?? DEFAULT_POLICY.exitTimelockSeconds,
|
|
17
11
|
};
|
|
18
12
|
if (params.totalCapacity <= BigInt(0)) {
|
|
19
|
-
throw new
|
|
13
|
+
throw new VtxoPolicyError('Pool totalCapacity must be > 0');
|
|
20
14
|
}
|
|
21
|
-
const poolId =
|
|
15
|
+
const poolId = computePoolId({
|
|
22
16
|
operator: params.operator,
|
|
23
17
|
tokenId: params.tokenId,
|
|
24
18
|
nonce: params.nonce,
|
|
@@ -29,32 +23,32 @@ function createPool(params, now) {
|
|
|
29
23
|
tokenId: params.tokenId,
|
|
30
24
|
totalCapacity: params.totalCapacity,
|
|
31
25
|
availableCapacity: params.totalCapacity,
|
|
32
|
-
epoch:
|
|
33
|
-
commitmentRoot:
|
|
26
|
+
epoch: EPOCH_ZERO,
|
|
27
|
+
commitmentRoot: EMPTY_TREE_ROOT,
|
|
34
28
|
createdAt: now,
|
|
35
29
|
policy,
|
|
36
30
|
};
|
|
37
31
|
}
|
|
38
|
-
function assertPoolCanMint(pool, amount) {
|
|
32
|
+
export function assertPoolCanMint(pool, amount) {
|
|
39
33
|
if (amount <= BigInt(0)) {
|
|
40
|
-
throw new
|
|
34
|
+
throw new VtxoPolicyError(`Mint amount must be > 0, got ${amount}`);
|
|
41
35
|
}
|
|
42
36
|
if (amount < pool.policy.minAmount) {
|
|
43
|
-
throw new
|
|
37
|
+
throw new VtxoPolicyError(`Mint amount ${amount} is below pool minimum ${pool.policy.minAmount}`);
|
|
44
38
|
}
|
|
45
39
|
if (amount > pool.policy.maxAmount) {
|
|
46
|
-
throw new
|
|
40
|
+
throw new VtxoPolicyError(`Mint amount ${amount} exceeds pool maximum ${pool.policy.maxAmount}`);
|
|
47
41
|
}
|
|
48
42
|
if (amount > pool.availableCapacity) {
|
|
49
|
-
throw new
|
|
43
|
+
throw new VtxoPoolCapacityError(`Insufficient pool capacity: requested ${amount}, available ${pool.availableCapacity}`, amount, pool.availableCapacity);
|
|
50
44
|
}
|
|
51
45
|
}
|
|
52
|
-
function updatePoolRoot(pool, root) {
|
|
46
|
+
export function updatePoolRoot(pool, root) {
|
|
53
47
|
return { ...pool, commitmentRoot: root };
|
|
54
48
|
}
|
|
55
|
-
function advancePoolEpoch(pool, newEpoch) {
|
|
49
|
+
export function advancePoolEpoch(pool, newEpoch) {
|
|
56
50
|
if (newEpoch <= pool.epoch) {
|
|
57
|
-
throw new
|
|
51
|
+
throw new VtxoPolicyError(`New epoch ${newEpoch} must be greater than current epoch ${pool.epoch}`);
|
|
58
52
|
}
|
|
59
53
|
return { ...pool, epoch: newEpoch };
|
|
60
54
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VTXO receipt co-signing (RFC-038 V0, verify-before-sign per §3.5).
|
|
3
|
+
*
|
|
4
|
+
* A mutation emits its `VtxoOperatorReceipt` **unsigned**; the operator co-signs
|
|
5
|
+
* it only after **recomputing** the batch root from the batch's leaves and
|
|
6
|
+
* checking epoch monotonicity, output membership, and (when supplied) amount
|
|
7
|
+
* conservation. This prevents a caller from getting the operator to co-sign an
|
|
8
|
+
* arbitrary `(root, epoch, ids)` tuple whose root does not commit the outputs.
|
|
9
|
+
*/
|
|
10
|
+
import type { OmniaVtxo, OmniaVtxoOperator, VtxoOperatorReceipt } from './types.js';
|
|
11
|
+
/** The batch the receipt belongs to — the operator's source of truth. */
|
|
12
|
+
export interface VtxoReceiptBatch {
|
|
13
|
+
/** The VTXOs committed in this epoch's batch. */
|
|
14
|
+
outputs: OmniaVtxo[];
|
|
15
|
+
/** The batch root the receipt claims (the operator recomputes and compares). */
|
|
16
|
+
root: string;
|
|
17
|
+
/** The batch epoch. */
|
|
18
|
+
epoch: number;
|
|
19
|
+
/** The pool's current epoch, for monotonicity (must be strictly less). */
|
|
20
|
+
previousEpoch?: number;
|
|
21
|
+
/** Optional: total input amount, for a conservation check. */
|
|
22
|
+
inputsAmount?: bigint;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Co-sign a receipt with the operator, after verifying the batch.
|
|
26
|
+
* @throws if the root does not commit the leaves, the epoch is stale, an output
|
|
27
|
+
* is not in the batch, or conservation fails.
|
|
28
|
+
*/
|
|
29
|
+
export declare function signVtxoReceipt(operator: OmniaVtxoOperator, receipt: VtxoOperatorReceipt, batch: VtxoReceiptBatch): Promise<VtxoOperatorReceipt>;
|
|
30
|
+
/** Verify a co-signed receipt against the operator and the batch root. */
|
|
31
|
+
export declare function verifyVtxoReceipt(operator: OmniaVtxoOperator, receipt: VtxoOperatorReceipt, root: string): Promise<boolean>;
|
package/dist/receipt.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VTXO receipt co-signing (RFC-038 V0, verify-before-sign per §3.5).
|
|
3
|
+
*
|
|
4
|
+
* A mutation emits its `VtxoOperatorReceipt` **unsigned**; the operator co-signs
|
|
5
|
+
* it only after **recomputing** the batch root from the batch's leaves and
|
|
6
|
+
* checking epoch monotonicity, output membership, and (when supplied) amount
|
|
7
|
+
* conservation. This prevents a caller from getting the operator to co-sign an
|
|
8
|
+
* arbitrary `(root, epoch, ids)` tuple whose root does not commit the outputs.
|
|
9
|
+
*/
|
|
10
|
+
import { computeBatchCommitmentBytes, computeCommitmentRoot, computeVtxoLeaf } from './commitment.js';
|
|
11
|
+
/**
|
|
12
|
+
* Co-sign a receipt with the operator, after verifying the batch.
|
|
13
|
+
* @throws if the root does not commit the leaves, the epoch is stale, an output
|
|
14
|
+
* is not in the batch, or conservation fails.
|
|
15
|
+
*/
|
|
16
|
+
export async function signVtxoReceipt(operator, receipt, batch) {
|
|
17
|
+
if (receipt.epoch !== batch.epoch) {
|
|
18
|
+
throw new Error(`receipt epoch ${receipt.epoch} does not match batch epoch ${batch.epoch}`);
|
|
19
|
+
}
|
|
20
|
+
if (batch.previousEpoch !== undefined && batch.epoch <= batch.previousEpoch) {
|
|
21
|
+
throw new Error(`batch epoch ${batch.epoch} is not greater than the pool epoch ${batch.previousEpoch}`);
|
|
22
|
+
}
|
|
23
|
+
const idSet = new Set(batch.outputs.map((v) => v.vtxoId));
|
|
24
|
+
for (const id of receipt.outputIds) {
|
|
25
|
+
if (!idSet.has(id))
|
|
26
|
+
throw new Error(`receipt output '${id}' is not a VTXO in the batch`);
|
|
27
|
+
}
|
|
28
|
+
const recomputedRoot = computeCommitmentRoot(batch.outputs.map(computeVtxoLeaf));
|
|
29
|
+
if (recomputedRoot !== batch.root) {
|
|
30
|
+
throw new Error('batch root does not commit the batch leaves');
|
|
31
|
+
}
|
|
32
|
+
if (batch.inputsAmount !== undefined) {
|
|
33
|
+
const outputSum = batch.outputs.reduce((sum, v) => sum + v.amount, 0n);
|
|
34
|
+
if (outputSum > batch.inputsAmount) {
|
|
35
|
+
throw new Error(`conservation violated: outputs ${outputSum} exceed inputs ${batch.inputsAmount}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const data = computeBatchCommitmentBytes(receipt.poolId, receipt.epoch, batch.root, receipt.outputIds);
|
|
39
|
+
const signature = await operator.sign(data);
|
|
40
|
+
return { ...receipt, signature };
|
|
41
|
+
}
|
|
42
|
+
/** Verify a co-signed receipt against the operator and the batch root. */
|
|
43
|
+
export async function verifyVtxoReceipt(operator, receipt, root) {
|
|
44
|
+
if (!receipt.signature)
|
|
45
|
+
return false;
|
|
46
|
+
const data = computeBatchCommitmentBytes(receipt.poolId, receipt.epoch, root, receipt.outputIds);
|
|
47
|
+
return operator.verify(data, receipt.signature);
|
|
48
|
+
}
|
package/dist/refresh.js
CHANGED
|
@@ -1,22 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.refreshVtxo = refreshVtxo;
|
|
4
|
-
const errors_js_1 = require("./errors.js");
|
|
5
|
-
const commitment_js_1 = require("./commitment.js");
|
|
6
|
-
const constants_js_1 = require("./constants.js");
|
|
1
|
+
import { VtxoStatusError, VtxoPolicyError } from './errors.js';
|
|
2
|
+
import { computeVtxoId, buildVtxoProofSet, computeReceiptId } from './commitment.js';
|
|
7
3
|
/**
|
|
8
4
|
* Refreshes a VTXO to a new epoch.
|
|
9
5
|
* @param now - Timestamp in ms. Defaults to Date.now() if omitted — pass explicitly for determinism.
|
|
10
6
|
*/
|
|
11
|
-
function refreshVtxo(vtxo, params, now) {
|
|
7
|
+
export function refreshVtxo(vtxo, params, now) {
|
|
12
8
|
const ts = now !== undefined ? now : Date.now();
|
|
13
9
|
if (vtxo.status !== 'active') {
|
|
14
|
-
throw new
|
|
10
|
+
throw new VtxoStatusError(`Cannot refresh VTXO ${vtxo.vtxoId}: status is '${vtxo.status}'`, vtxo.status);
|
|
15
11
|
}
|
|
16
12
|
if (params.newEpoch <= vtxo.epoch) {
|
|
17
|
-
throw new
|
|
13
|
+
throw new VtxoPolicyError(`Refresh epoch ${params.newEpoch} must be strictly greater than current epoch ${vtxo.epoch}`);
|
|
18
14
|
}
|
|
19
|
-
const refreshedId =
|
|
15
|
+
const refreshedId = computeVtxoId({
|
|
20
16
|
poolId: vtxo.poolId,
|
|
21
17
|
owner: vtxo.owner,
|
|
22
18
|
amount: vtxo.amount,
|
|
@@ -51,7 +47,7 @@ function refreshVtxo(vtxo, params, now) {
|
|
|
51
47
|
siblings: [],
|
|
52
48
|
positions: [],
|
|
53
49
|
epoch: params.newEpoch,
|
|
54
|
-
batchId:
|
|
50
|
+
batchId: '',
|
|
55
51
|
},
|
|
56
52
|
history: [
|
|
57
53
|
{
|
|
@@ -65,9 +61,9 @@ function refreshVtxo(vtxo, params, now) {
|
|
|
65
61
|
updatedAt: ts,
|
|
66
62
|
expiresAt: vtxo.expiresAt,
|
|
67
63
|
};
|
|
68
|
-
const { proofs } =
|
|
64
|
+
const { proofs } = buildVtxoProofSet([rawRefreshed]);
|
|
69
65
|
const refreshedVtxo = { ...rawRefreshed, proof: proofs[refreshedId] };
|
|
70
|
-
const receiptId =
|
|
66
|
+
const receiptId = computeReceiptId(vtxo.poolId, 'refresh', [vtxo.vtxoId], [refreshedId], ts);
|
|
71
67
|
return {
|
|
72
68
|
old: oldVtxo,
|
|
73
69
|
refreshed: refreshedVtxo,
|
|
@@ -79,7 +75,7 @@ function refreshVtxo(vtxo, params, now) {
|
|
|
79
75
|
outputIds: [refreshedId],
|
|
80
76
|
epoch: params.newEpoch,
|
|
81
77
|
at: ts,
|
|
82
|
-
signature:
|
|
78
|
+
signature: '',
|
|
83
79
|
},
|
|
84
80
|
};
|
|
85
81
|
}
|
package/dist/serialization.js
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.serializeVtxo = serializeVtxo;
|
|
4
|
-
exports.deserializeVtxo = deserializeVtxo;
|
|
5
|
-
exports.serializePool = serializePool;
|
|
6
|
-
exports.deserializePool = deserializePool;
|
|
7
1
|
const BIGINT_PREFIX = '__bigint__:';
|
|
8
2
|
function replacer(_key, value) {
|
|
9
3
|
if (typeof value === 'bigint') {
|
|
@@ -17,15 +11,15 @@ function reviver(_key, value) {
|
|
|
17
11
|
}
|
|
18
12
|
return value;
|
|
19
13
|
}
|
|
20
|
-
function serializeVtxo(vtxo) {
|
|
14
|
+
export function serializeVtxo(vtxo) {
|
|
21
15
|
return JSON.stringify(vtxo, replacer);
|
|
22
16
|
}
|
|
23
|
-
function deserializeVtxo(json) {
|
|
17
|
+
export function deserializeVtxo(json) {
|
|
24
18
|
return JSON.parse(json, reviver);
|
|
25
19
|
}
|
|
26
|
-
function serializePool(pool) {
|
|
20
|
+
export function serializePool(pool) {
|
|
27
21
|
return JSON.stringify(pool, replacer);
|
|
28
22
|
}
|
|
29
|
-
function deserializePool(json) {
|
|
23
|
+
export function deserializePool(json) {
|
|
30
24
|
return JSON.parse(json, reviver);
|
|
31
25
|
}
|
package/dist/split.js
CHANGED
|
@@ -1,34 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.splitVtxo = splitVtxo;
|
|
4
|
-
const errors_js_1 = require("./errors.js");
|
|
5
|
-
const commitment_js_1 = require("./commitment.js");
|
|
6
|
-
const constants_js_1 = require("./constants.js");
|
|
1
|
+
import { VtxoStatusError, VtxoAmountError, VtxoSplitError } from './errors.js';
|
|
2
|
+
import { computeVtxoId, buildVtxoProofSet, computeReceiptId } from './commitment.js';
|
|
7
3
|
/**
|
|
8
4
|
* Splits a VTXO into multiple outputs.
|
|
9
5
|
* @param now - Timestamp in ms. Defaults to Date.now() if omitted — pass explicitly for determinism.
|
|
10
6
|
*/
|
|
11
|
-
function splitVtxo(vtxo, params, now) {
|
|
7
|
+
export function splitVtxo(vtxo, params, now) {
|
|
12
8
|
const ts = now !== undefined ? now : Date.now();
|
|
13
9
|
if (vtxo.status !== 'active') {
|
|
14
|
-
throw new
|
|
10
|
+
throw new VtxoStatusError(`Cannot split VTXO ${vtxo.vtxoId}: status is '${vtxo.status}'`, vtxo.status);
|
|
15
11
|
}
|
|
16
12
|
if (params.amounts.length < 2) {
|
|
17
|
-
throw new
|
|
13
|
+
throw new VtxoSplitError('Split requires at least 2 output amounts');
|
|
18
14
|
}
|
|
19
15
|
if (params.amounts.length !== params.nonces.length) {
|
|
20
|
-
throw new
|
|
16
|
+
throw new VtxoSplitError('amounts and nonces arrays must have the same length');
|
|
21
17
|
}
|
|
22
18
|
for (const amt of params.amounts) {
|
|
23
19
|
if (amt <= BigInt(0)) {
|
|
24
|
-
throw new
|
|
20
|
+
throw new VtxoAmountError(`All split amounts must be > 0, got ${amt}`);
|
|
25
21
|
}
|
|
26
22
|
}
|
|
27
23
|
const sum = params.amounts.reduce((a, b) => a + b, BigInt(0));
|
|
28
24
|
if (sum !== vtxo.amount) {
|
|
29
|
-
throw new
|
|
25
|
+
throw new VtxoAmountError(`Split amounts sum ${sum} does not equal VTXO amount ${vtxo.amount}`);
|
|
30
26
|
}
|
|
31
|
-
const outputIds = params.amounts.map((amount, i) =>
|
|
27
|
+
const outputIds = params.amounts.map((amount, i) => computeVtxoId({
|
|
32
28
|
poolId: vtxo.poolId,
|
|
33
29
|
owner: vtxo.owner,
|
|
34
30
|
amount,
|
|
@@ -63,7 +59,7 @@ function splitVtxo(vtxo, params, now) {
|
|
|
63
59
|
siblings: [],
|
|
64
60
|
positions: [],
|
|
65
61
|
epoch: vtxo.epoch,
|
|
66
|
-
batchId:
|
|
62
|
+
batchId: '',
|
|
67
63
|
},
|
|
68
64
|
history: [
|
|
69
65
|
{
|
|
@@ -77,9 +73,9 @@ function splitVtxo(vtxo, params, now) {
|
|
|
77
73
|
createdAt: ts,
|
|
78
74
|
updatedAt: ts,
|
|
79
75
|
}));
|
|
80
|
-
const { proofs } =
|
|
76
|
+
const { proofs } = buildVtxoProofSet(rawOutputs);
|
|
81
77
|
const outputs = rawOutputs.map(o => ({ ...o, proof: proofs[o.vtxoId] }));
|
|
82
|
-
const receiptId =
|
|
78
|
+
const receiptId = computeReceiptId(vtxo.poolId, 'split', [vtxo.vtxoId], outputIds, ts);
|
|
83
79
|
return {
|
|
84
80
|
input: inputVtxo,
|
|
85
81
|
outputs,
|
|
@@ -91,7 +87,7 @@ function splitVtxo(vtxo, params, now) {
|
|
|
91
87
|
outputIds,
|
|
92
88
|
epoch: vtxo.epoch,
|
|
93
89
|
at: ts,
|
|
94
|
-
signature:
|
|
90
|
+
signature: '',
|
|
95
91
|
},
|
|
96
92
|
};
|
|
97
93
|
}
|