@totemsdk/omnia-vtxo 0.6.1 → 0.7.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 +202 -21
- package/NOTICE +60 -0
- package/README.md +1 -1
- package/dist/anchor.d.ts +14 -2
- package/dist/anchor.js +33 -10
- package/dist/cjs/anchor.d.ts +14 -2
- package/dist/cjs/anchor.js +32 -9
- package/dist/cjs/claim-policy.d.ts +94 -0
- package/dist/cjs/claim-policy.js +149 -0
- package/dist/cjs/commitment.d.ts +12 -0
- package/dist/cjs/commitment.js +21 -1
- package/dist/cjs/durable-store.js +8 -0
- package/dist/cjs/exit-entitlement.d.ts +35 -0
- package/dist/cjs/exit-entitlement.js +70 -0
- package/dist/cjs/exit-tx.d.ts +6 -0
- package/dist/cjs/exit-tx.js +31 -10
- package/dist/cjs/index.d.ts +4 -0
- package/dist/cjs/index.js +4 -0
- package/dist/cjs/memory-store.js +8 -0
- package/dist/cjs/owner-pool.d.ts +95 -0
- package/dist/cjs/owner-pool.js +194 -0
- package/dist/cjs/receipt.d.ts +8 -1
- package/dist/cjs/receipt.js +29 -7
- package/dist/cjs/transfer.d.ts +20 -0
- package/dist/cjs/transfer.js +39 -0
- package/dist/cjs/types.d.ts +8 -0
- package/dist/cjs/yield.d.ts +2 -0
- package/dist/cjs/yield.js +26 -0
- package/dist/claim-policy.d.ts +94 -0
- package/dist/claim-policy.js +144 -0
- package/dist/commitment.d.ts +12 -0
- package/dist/commitment.js +18 -0
- package/dist/durable-store.js +8 -0
- package/dist/exit-entitlement.d.ts +35 -0
- package/dist/exit-entitlement.js +66 -0
- package/dist/exit-tx.d.ts +6 -0
- package/dist/exit-tx.js +32 -11
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/memory-store.js +8 -0
- package/dist/owner-pool.d.ts +95 -0
- package/dist/owner-pool.js +183 -0
- package/dist/receipt.d.ts +8 -1
- package/dist/receipt.js +28 -8
- package/dist/transfer.d.ts +20 -0
- package/dist/transfer.js +38 -0
- package/dist/types.d.ts +8 -0
- package/dist/yield.d.ts +2 -0
- package/dist/yield.js +23 -0
- package/package.json +9 -7
- package/rust/pkg/package.json +1 -1
- package/rust/pkg-node/package.json +1 -1
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* V1 transferable owner-pool covenant.
|
|
4
|
+
*
|
|
5
|
+
* A single-owner claim coin whose **current owner is committed in state**, not
|
|
6
|
+
* in the script. The owner can:
|
|
7
|
+
*
|
|
8
|
+
* - transfer the whole coin by signing a state update that recreates it at
|
|
9
|
+
* the same address/amount with a new owner (epoch increments); or
|
|
10
|
+
* - exit after `exitBlock` to the committed owner address, with no operator
|
|
11
|
+
* signature or liveness.
|
|
12
|
+
*
|
|
13
|
+
* The script authenticates the **previous** committed owner
|
|
14
|
+
* (`SIGNEDBY(PREVSTATE(103))`), so a spender cannot substitute their own key.
|
|
15
|
+
* `PREVSTATE(104)` is the committed payout address. This is the V1 recovery
|
|
16
|
+
* path: a recipient holds a coin they can spend unilaterally.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.OWNER_POOL_ADDR_PORT = exports.OWNER_POOL_PK_PORT = exports.OWNER_POOL_EXIT_BLOCK_PORT = exports.OWNER_POOL_EPOCH_PORT = exports.OWNER_POOL_ROOT_PORT = void 0;
|
|
20
|
+
exports.ownerAddressFor = ownerAddressFor;
|
|
21
|
+
exports.buildOwnerPoolScript = buildOwnerPoolScript;
|
|
22
|
+
exports.buildOwnerPoolState = buildOwnerPoolState;
|
|
23
|
+
exports.ownerPoolAddress = ownerPoolAddress;
|
|
24
|
+
exports.buildOwnerTransferDraft = buildOwnerTransferDraft;
|
|
25
|
+
exports.buildOwnerExitDraft = buildOwnerExitDraft;
|
|
26
|
+
exports.buildOwnerTransferTransaction = buildOwnerTransferTransaction;
|
|
27
|
+
exports.buildOwnerExitTransaction = buildOwnerExitTransaction;
|
|
28
|
+
const core_1 = require("@totemsdk/core");
|
|
29
|
+
exports.OWNER_POOL_ROOT_PORT = 100;
|
|
30
|
+
exports.OWNER_POOL_EPOCH_PORT = 101;
|
|
31
|
+
exports.OWNER_POOL_EXIT_BLOCK_PORT = 102;
|
|
32
|
+
exports.OWNER_POOL_PK_PORT = 103;
|
|
33
|
+
exports.OWNER_POOL_ADDR_PORT = 104;
|
|
34
|
+
/** Derive the committed payout address from an owner public-key digest. */
|
|
35
|
+
function ownerAddressFor(ownerPk) {
|
|
36
|
+
return (0, core_1.wotsAddressData)(ownerPk);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The transferable owner-pool script. State ports 100–104 are the only state;
|
|
40
|
+
* there are no fixed holder constants.
|
|
41
|
+
*/
|
|
42
|
+
function buildOwnerPoolScript() {
|
|
43
|
+
return (0, core_1.toNodeScript)([
|
|
44
|
+
// Unilateral exit: only the previously committed owner, only after the
|
|
45
|
+
// committed block, and only to the committed owner address.
|
|
46
|
+
'IF @BLOCK GT PREVSTATE(102) THEN',
|
|
47
|
+
' ASSERT SIGNEDBY(PREVSTATE(103))',
|
|
48
|
+
' ASSERT VERIFYOUT(0 PREVSTATE(104) @AMOUNT @TOKENID FALSE)',
|
|
49
|
+
' RETURN TRUE',
|
|
50
|
+
'ENDIF',
|
|
51
|
+
// Transfer: the previous owner authorizes a new owner. Root and exit block
|
|
52
|
+
// are immutable; the coin stays at the same address with the same amount.
|
|
53
|
+
'ASSERT SIGNEDBY(PREVSTATE(103))',
|
|
54
|
+
'ASSERT STATE(101) GT PREVSTATE(101)',
|
|
55
|
+
'ASSERT STATE(100) EQ PREVSTATE(100)',
|
|
56
|
+
'ASSERT STATE(102) EQ PREVSTATE(102)',
|
|
57
|
+
'ASSERT STATE(103) NEQ PREVSTATE(103)',
|
|
58
|
+
'ASSERT STATE(103) NEQ 0x00',
|
|
59
|
+
'ASSERT STATE(104) NEQ 0x00',
|
|
60
|
+
'ASSERT VERIFYOUT(0 @ADDRESS @AMOUNT @TOKENID TRUE)',
|
|
61
|
+
'RETURN TRUE',
|
|
62
|
+
].join('\n'));
|
|
63
|
+
}
|
|
64
|
+
function buildOwnerPoolState(state) {
|
|
65
|
+
if (!Number.isInteger(state.exitBlock) || state.exitBlock < 0) {
|
|
66
|
+
throw new Error(`owner pool: invalid exitBlock ${String(state.exitBlock)}`);
|
|
67
|
+
}
|
|
68
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(state.ownerPk)) {
|
|
69
|
+
throw new Error('owner pool: ownerPk must be a 0x-prefixed 32-byte hex digest');
|
|
70
|
+
}
|
|
71
|
+
if (!state.ownerAddr)
|
|
72
|
+
throw new Error('owner pool: ownerAddr is required');
|
|
73
|
+
return [
|
|
74
|
+
{ port: exports.OWNER_POOL_ROOT_PORT, value: state.root, type: 'string' },
|
|
75
|
+
{ port: exports.OWNER_POOL_EPOCH_PORT, value: BigInt(state.epoch), type: 'number' },
|
|
76
|
+
{ port: exports.OWNER_POOL_EXIT_BLOCK_PORT, value: BigInt(state.exitBlock), type: 'number' },
|
|
77
|
+
{ port: exports.OWNER_POOL_PK_PORT, value: state.ownerPk, type: 'string' },
|
|
78
|
+
{ port: exports.OWNER_POOL_ADDR_PORT, value: state.ownerAddr, type: 'string' },
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
function ownerPoolAddress() {
|
|
82
|
+
return (0, core_1.computeScriptAddress)(buildOwnerPoolScript());
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Build the transfer transaction: same script address, same amount/token,
|
|
86
|
+
* new owner state and a higher epoch.
|
|
87
|
+
*/
|
|
88
|
+
function buildOwnerTransferDraft(params) {
|
|
89
|
+
if (params.to.epoch <= params.from.epoch) {
|
|
90
|
+
throw new Error('owner transfer: new epoch must be greater than the previous epoch');
|
|
91
|
+
}
|
|
92
|
+
if (params.to.exitBlock !== params.from.exitBlock) {
|
|
93
|
+
throw new Error('owner transfer: exitBlock is immutable');
|
|
94
|
+
}
|
|
95
|
+
if (params.to.ownerAddr !== ownerAddressFor(params.to.ownerPk)) {
|
|
96
|
+
throw new Error('owner transfer: ownerAddr must be derived from ownerPk');
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
type: 'settlement',
|
|
100
|
+
inputs: [{
|
|
101
|
+
coinId: params.coin.coinId,
|
|
102
|
+
address: params.coin.address,
|
|
103
|
+
amount: params.coin.amount,
|
|
104
|
+
tokenId: params.coin.tokenId,
|
|
105
|
+
scriptHex: params.coin.scriptHex ?? buildOwnerPoolScript(),
|
|
106
|
+
...(params.coin.mmrEntry !== undefined ? { mmrEntry: params.coin.mmrEntry } : {}),
|
|
107
|
+
}],
|
|
108
|
+
outputs: [{
|
|
109
|
+
address: params.coin.address,
|
|
110
|
+
amount: params.coin.amount,
|
|
111
|
+
tokenId: params.coin.tokenId,
|
|
112
|
+
storeState: true,
|
|
113
|
+
stateVariables: buildOwnerPoolState(params.to),
|
|
114
|
+
}],
|
|
115
|
+
storeState: true,
|
|
116
|
+
stateVariables: buildOwnerPoolState(params.to),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** Build the unilateral exit: full amount to the committed owner address. */
|
|
120
|
+
function buildOwnerExitDraft(params) {
|
|
121
|
+
if (params.state.ownerAddr !== ownerAddressFor(params.state.ownerPk)) {
|
|
122
|
+
throw new Error('owner exit: ownerAddr must be derived from ownerPk');
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
type: 'settlement',
|
|
126
|
+
inputs: [{
|
|
127
|
+
coinId: params.coin.coinId,
|
|
128
|
+
address: params.coin.address,
|
|
129
|
+
amount: params.coin.amount,
|
|
130
|
+
tokenId: params.coin.tokenId,
|
|
131
|
+
scriptHex: params.coin.scriptHex ?? buildOwnerPoolScript(),
|
|
132
|
+
...(params.coin.mmrEntry !== undefined ? { mmrEntry: params.coin.mmrEntry } : {}),
|
|
133
|
+
}],
|
|
134
|
+
outputs: [{
|
|
135
|
+
address: params.state.ownerAddr,
|
|
136
|
+
amount: params.coin.amount,
|
|
137
|
+
tokenId: params.coin.tokenId,
|
|
138
|
+
storeState: false,
|
|
139
|
+
stateVariables: [],
|
|
140
|
+
}],
|
|
141
|
+
storeState: false,
|
|
142
|
+
stateVariables: [],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// ── Signed transaction assembly ───────────────────────────────────────────────
|
|
146
|
+
const omnia_1 = require("@totemsdk/omnia");
|
|
147
|
+
const core_2 = require("@totemsdk/core");
|
|
148
|
+
const txpow_1 = require("@totemsdk/txpow");
|
|
149
|
+
async function assembleOwnerPoolTx(draft, kind, state, deps) {
|
|
150
|
+
const digest = (0, omnia_1.computeOmniaTxDigest)(draft);
|
|
151
|
+
const signature = await deps.signDigest(digest);
|
|
152
|
+
const witness = (0, omnia_1.serializeOmniaWitness)({
|
|
153
|
+
signatures: [signature],
|
|
154
|
+
coinProofs: deps.coinProofs ?? [],
|
|
155
|
+
scriptProofs: deps.scriptProofs ?? [],
|
|
156
|
+
});
|
|
157
|
+
const result = {
|
|
158
|
+
kind,
|
|
159
|
+
txHex: (0, omnia_1.serializeTxDraft)(draft),
|
|
160
|
+
digest: Buffer.from(digest).toString('hex'),
|
|
161
|
+
witnessHex: Buffer.from(witness).toString('hex'),
|
|
162
|
+
state,
|
|
163
|
+
};
|
|
164
|
+
if (deps.chainProvider) {
|
|
165
|
+
const draftBytes = (0, omnia_1.omniaDraftToMinimaBytes)(draft);
|
|
166
|
+
if (typeof deps.chainProvider.submitTransaction === 'function') {
|
|
167
|
+
const submitted = await deps.chainProvider.submitTransaction(draftBytes, witness);
|
|
168
|
+
if (submitted?.success === false) {
|
|
169
|
+
throw new Error(`owner-pool submit failed: ${submitted.message ?? 'unknown error'}`);
|
|
170
|
+
}
|
|
171
|
+
result.txpowId = submitted?.txpowid;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const mine = deps.mine ?? ((txBody, difficulty) => (0, txpow_1.mineTxPoW)(txBody, difficulty));
|
|
175
|
+
const txBody = (0, txpow_1.serializeTxBody)(draftBytes, witness);
|
|
176
|
+
const mined = await mine(txBody, txpow_1.TX_POW_MIN_DIFFICULTY);
|
|
177
|
+
const fullTxPoW = (0, core_2.concatBytes)((0, core_2.concatBytes)(mined.minedHeaderBytes, new Uint8Array([0x01])), txBody);
|
|
178
|
+
const broadcast = await deps.chainProvider.broadcastTxPoW(Buffer.from(fullTxPoW).toString('hex'));
|
|
179
|
+
if (broadcast?.success === false) {
|
|
180
|
+
throw new Error(`owner-pool broadcast failed: ${broadcast.message ?? 'unknown error'}`);
|
|
181
|
+
}
|
|
182
|
+
result.txpowId = broadcast?.txpowid ?? Buffer.from(mined.txpowId).toString('hex');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
/** Sign and assemble the owner-rotation transfer transaction. */
|
|
188
|
+
async function buildOwnerTransferTransaction(params, deps) {
|
|
189
|
+
return assembleOwnerPoolTx(buildOwnerTransferDraft(params), 'owner-transfer', params.to, deps);
|
|
190
|
+
}
|
|
191
|
+
/** Sign and assemble the unilateral owner exit transaction. */
|
|
192
|
+
async function buildOwnerExitTransaction(params, deps) {
|
|
193
|
+
return assembleOwnerPoolTx(buildOwnerExitDraft(params), 'owner-exit', params.state, deps);
|
|
194
|
+
}
|
package/dist/cjs/receipt.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* conservation. This prevents a caller from getting the operator to co-sign an
|
|
8
8
|
* arbitrary `(root, epoch, ids)` tuple whose root does not commit the outputs.
|
|
9
9
|
*/
|
|
10
|
-
import type { OmniaVtxo, OmniaVtxoOperator, VtxoOperatorReceipt } from './types.js';
|
|
10
|
+
import type { OmniaVtxo, OmniaVtxoOperator, OmniaVtxoPool, VtxoOperatorReceipt } from './types.js';
|
|
11
11
|
/** The batch the receipt belongs to — the operator's source of truth. */
|
|
12
12
|
export interface VtxoReceiptBatch {
|
|
13
13
|
/** The VTXOs committed in this epoch's batch. */
|
|
@@ -26,6 +26,13 @@ export interface VtxoReceiptBatch {
|
|
|
26
26
|
* @throws if the root does not commit the leaves, the epoch is stale, an output
|
|
27
27
|
* is not in the batch, or conservation fails.
|
|
28
28
|
*/
|
|
29
|
+
export declare function buildVtxoReceiptPayload(receipt: VtxoOperatorReceipt, batch: VtxoReceiptBatch): Uint8Array;
|
|
29
30
|
export declare function signVtxoReceipt(operator: OmniaVtxoOperator, receipt: VtxoOperatorReceipt, batch: VtxoReceiptBatch): Promise<VtxoOperatorReceipt>;
|
|
30
31
|
/** Verify a co-signed receipt against the operator and the batch root. */
|
|
31
32
|
export declare function verifyVtxoReceipt(operator: OmniaVtxoOperator, receipt: VtxoOperatorReceipt, root: string): Promise<boolean>;
|
|
33
|
+
/**
|
|
34
|
+
* Verify a receipt against the pool's current anchored state, not a
|
|
35
|
+
* caller-supplied root. Rejects a receipt from another pool/epoch and requires
|
|
36
|
+
* the pool's committed root to be present.
|
|
37
|
+
*/
|
|
38
|
+
export declare function verifyVtxoReceiptForPool(operator: OmniaVtxoOperator, receipt: VtxoOperatorReceipt, pool: Pick<OmniaVtxoPool, 'poolId' | 'epoch' | 'commitmentRoot'>): Promise<boolean>;
|
package/dist/cjs/receipt.js
CHANGED
|
@@ -9,15 +9,17 @@
|
|
|
9
9
|
* arbitrary `(root, epoch, ids)` tuple whose root does not commit the outputs.
|
|
10
10
|
*/
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.buildVtxoReceiptPayload = buildVtxoReceiptPayload;
|
|
12
13
|
exports.signVtxoReceipt = signVtxoReceipt;
|
|
13
14
|
exports.verifyVtxoReceipt = verifyVtxoReceipt;
|
|
15
|
+
exports.verifyVtxoReceiptForPool = verifyVtxoReceiptForPool;
|
|
14
16
|
const commitment_js_1 = require("./commitment.js");
|
|
15
17
|
/**
|
|
16
18
|
* Co-sign a receipt with the operator, after verifying the batch.
|
|
17
19
|
* @throws if the root does not commit the leaves, the epoch is stale, an output
|
|
18
20
|
* is not in the batch, or conservation fails.
|
|
19
21
|
*/
|
|
20
|
-
|
|
22
|
+
function buildVtxoReceiptPayload(receipt, batch) {
|
|
21
23
|
if (receipt.epoch !== batch.epoch) {
|
|
22
24
|
throw new Error(`receipt epoch ${receipt.epoch} does not match batch epoch ${batch.epoch}`);
|
|
23
25
|
}
|
|
@@ -33,13 +35,19 @@ async function signVtxoReceipt(operator, receipt, batch) {
|
|
|
33
35
|
if (recomputedRoot !== batch.root) {
|
|
34
36
|
throw new Error('batch root does not commit the batch leaves');
|
|
35
37
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
throw new Error(`conservation
|
|
38
|
+
const outputSum = batch.outputs.reduce((sum, v) => sum + v.amount, 0n);
|
|
39
|
+
if (receipt.op !== 'mint') {
|
|
40
|
+
if (batch.inputsAmount === undefined) {
|
|
41
|
+
throw new Error(`conservation required for ${receipt.op} receipts`);
|
|
42
|
+
}
|
|
43
|
+
if (outputSum !== batch.inputsAmount) {
|
|
44
|
+
throw new Error(`conservation violated: outputs ${outputSum} != inputs ${batch.inputsAmount}`);
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
|
-
|
|
47
|
+
return (0, commitment_js_1.computeVtxoReceiptCommitmentBytes)(receipt, batch.root);
|
|
48
|
+
}
|
|
49
|
+
async function signVtxoReceipt(operator, receipt, batch) {
|
|
50
|
+
const data = buildVtxoReceiptPayload(receipt, batch);
|
|
43
51
|
const signature = await operator.sign(data);
|
|
44
52
|
return { ...receipt, signature };
|
|
45
53
|
}
|
|
@@ -47,6 +55,20 @@ async function signVtxoReceipt(operator, receipt, batch) {
|
|
|
47
55
|
async function verifyVtxoReceipt(operator, receipt, root) {
|
|
48
56
|
if (!receipt.signature)
|
|
49
57
|
return false;
|
|
50
|
-
const data = (0, commitment_js_1.
|
|
58
|
+
const data = (0, commitment_js_1.computeVtxoReceiptCommitmentBytes)(receipt, root);
|
|
51
59
|
return operator.verify(data, receipt.signature);
|
|
52
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Verify a receipt against the pool's current anchored state, not a
|
|
63
|
+
* caller-supplied root. Rejects a receipt from another pool/epoch and requires
|
|
64
|
+
* the pool's committed root to be present.
|
|
65
|
+
*/
|
|
66
|
+
async function verifyVtxoReceiptForPool(operator, receipt, pool) {
|
|
67
|
+
if (receipt.poolId !== pool.poolId)
|
|
68
|
+
return false;
|
|
69
|
+
if (receipt.epoch !== pool.epoch)
|
|
70
|
+
return false;
|
|
71
|
+
if (!pool.commitmentRoot)
|
|
72
|
+
return false;
|
|
73
|
+
return verifyVtxoReceipt(operator, receipt, pool.commitmentRoot);
|
|
74
|
+
}
|
package/dist/cjs/transfer.d.ts
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
import { OmniaVtxo, TransferVtxoParams, TransferResult, VtxoOperatorReceipt } from './types.js';
|
|
2
|
+
import { type VtxoExitEntitlement } from './exit-entitlement.js';
|
|
3
|
+
import { type ExitTxParams, type ExitTxDeps } from './exit-tx.js';
|
|
2
4
|
/**
|
|
3
5
|
* Transfers a VTXO (fully or partially) to a recipient.
|
|
6
|
+
*
|
|
7
|
+
* **Offline safety.** The returned output VTXOs are new claims. A Merkle proof
|
|
8
|
+
* establishes membership in a committed set; it does NOT prove the input was
|
|
9
|
+
* never spent elsewhere. The local host rejects a second transfer through its
|
|
10
|
+
* shared state, but disconnected receivers do not share that state. Treat a
|
|
11
|
+
* received VTXO as final only after an anti-double-spend check (online/operator
|
|
12
|
+
* spend check, operator-co-signed transfer, or an explicitly risk-bounded
|
|
13
|
+
* policy), and have the operator commit the new outputs — they do not
|
|
14
|
+
* automatically belong to the previous pool root.
|
|
15
|
+
*
|
|
4
16
|
* @param now - Timestamp in ms. Defaults to Date.now() if omitted — pass explicitly for determinism.
|
|
5
17
|
*/
|
|
6
18
|
export declare function transferVtxo(vtxo: OmniaVtxo, params: TransferVtxoParams, now?: number): TransferResult & {
|
|
7
19
|
receipt: VtxoOperatorReceipt;
|
|
8
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* V1 transfer: build the recipient's independent exit before moving the claim,
|
|
23
|
+
* then transfer only if the exit package verifies against the input VTXO.
|
|
24
|
+
*/
|
|
25
|
+
export declare function transferVtxoWithExit(vtxo: OmniaVtxo, params: Omit<TransferVtxoParams, 'exitEntitlement'>, exitParams: ExitTxParams, exitDeps: ExitTxDeps, now?: number): Promise<TransferResult & {
|
|
26
|
+
receipt: VtxoOperatorReceipt;
|
|
27
|
+
exitEntitlement: VtxoExitEntitlement;
|
|
28
|
+
}>;
|
package/dist/cjs/transfer.js
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.transferVtxo = transferVtxo;
|
|
4
|
+
exports.transferVtxoWithExit = transferVtxoWithExit;
|
|
4
5
|
const errors_js_1 = require("./errors.js");
|
|
5
6
|
const commitment_js_1 = require("./commitment.js");
|
|
7
|
+
const exit_entitlement_js_1 = require("./exit-entitlement.js");
|
|
8
|
+
const exit_tx_js_1 = require("./exit-tx.js");
|
|
6
9
|
/**
|
|
7
10
|
* Transfers a VTXO (fully or partially) to a recipient.
|
|
11
|
+
*
|
|
12
|
+
* **Offline safety.** The returned output VTXOs are new claims. A Merkle proof
|
|
13
|
+
* establishes membership in a committed set; it does NOT prove the input was
|
|
14
|
+
* never spent elsewhere. The local host rejects a second transfer through its
|
|
15
|
+
* shared state, but disconnected receivers do not share that state. Treat a
|
|
16
|
+
* received VTXO as final only after an anti-double-spend check (online/operator
|
|
17
|
+
* spend check, operator-co-signed transfer, or an explicitly risk-bounded
|
|
18
|
+
* policy), and have the operator commit the new outputs — they do not
|
|
19
|
+
* automatically belong to the previous pool root.
|
|
20
|
+
*
|
|
8
21
|
* @param now - Timestamp in ms. Defaults to Date.now() if omitted — pass explicitly for determinism.
|
|
9
22
|
*/
|
|
10
23
|
function transferVtxo(vtxo, params, now) {
|
|
@@ -18,6 +31,15 @@ function transferVtxo(vtxo, params, now) {
|
|
|
18
31
|
if (params.amount > vtxo.amount) {
|
|
19
32
|
throw new errors_js_1.VtxoAmountError(`Transfer amount ${params.amount} exceeds VTXO amount ${vtxo.amount}`);
|
|
20
33
|
}
|
|
34
|
+
if (params.exitEntitlement) {
|
|
35
|
+
if (params.amount !== vtxo.amount) {
|
|
36
|
+
throw new errors_js_1.VtxoAmountError('exit entitlement requires a full-value transfer');
|
|
37
|
+
}
|
|
38
|
+
const check = (0, exit_entitlement_js_1.verifyVtxoExitEntitlement)(params.exitEntitlement, vtxo);
|
|
39
|
+
if (!check.valid) {
|
|
40
|
+
throw new errors_js_1.VtxoAmountError(`invalid exit entitlement: ${check.errors.join('; ')}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
21
43
|
const isPartial = params.amount < vtxo.amount;
|
|
22
44
|
const changeAmount = vtxo.amount - params.amount;
|
|
23
45
|
const outputId = (0, commitment_js_1.computeVtxoId)({
|
|
@@ -62,6 +84,7 @@ function transferVtxo(vtxo, params, now) {
|
|
|
62
84
|
tokenId: vtxo.tokenId,
|
|
63
85
|
status: 'active',
|
|
64
86
|
epoch: vtxo.epoch,
|
|
87
|
+
...(params.exitEntitlement ? { exitEntitlement: params.exitEntitlement } : {}),
|
|
65
88
|
proof: {
|
|
66
89
|
leaf: '',
|
|
67
90
|
root: '',
|
|
@@ -150,3 +173,19 @@ function transferVtxo(vtxo, params, now) {
|
|
|
150
173
|
},
|
|
151
174
|
};
|
|
152
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* V1 transfer: build the recipient's independent exit before moving the claim,
|
|
178
|
+
* then transfer only if the exit package verifies against the input VTXO.
|
|
179
|
+
*/
|
|
180
|
+
async function transferVtxoWithExit(vtxo, params, exitParams, exitDeps, now) {
|
|
181
|
+
if (exitParams.vtxo.vtxoId !== vtxo.vtxoId) {
|
|
182
|
+
throw new errors_js_1.VtxoAmountError('exit entitlement VTXO does not match transfer input');
|
|
183
|
+
}
|
|
184
|
+
if (params.amount !== vtxo.amount) {
|
|
185
|
+
throw new errors_js_1.VtxoAmountError('transferVtxoWithExit requires a full-value transfer');
|
|
186
|
+
}
|
|
187
|
+
const exit = await (0, exit_tx_js_1.buildExitTransaction)(exitParams, exitDeps);
|
|
188
|
+
const entitlement = (0, exit_entitlement_js_1.createVtxoExitEntitlement)(exitParams, exit);
|
|
189
|
+
const transferred = transferVtxo(vtxo, { ...params, exitEntitlement: entitlement }, now);
|
|
190
|
+
return { ...transferred, exitEntitlement: entitlement };
|
|
191
|
+
}
|
package/dist/cjs/types.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { VtxoClaimPolicy } from './claim-policy.js';
|
|
2
|
+
import type { VtxoExitEntitlement } from './exit-entitlement.js';
|
|
1
3
|
export type VtxoId = string;
|
|
2
4
|
export type VtxoStatus = 'active' | 'transferred' | 'split' | 'merged' | 'refreshed' | 'exiting' | 'exited' | 'spent' | 'expired' | 'invalid';
|
|
3
5
|
export type VtxoOp = 'mint' | 'transfer' | 'split' | 'merge' | 'refresh' | 'exit_initiated' | 'exit_complete' | 'spent';
|
|
@@ -50,6 +52,10 @@ export interface OmniaVtxo {
|
|
|
50
52
|
expiresAt?: number;
|
|
51
53
|
/** The on-chain funding proof recorded at mint (deep proof, #28). */
|
|
52
54
|
fundingProof?: unknown;
|
|
55
|
+
/** Opt-in multiparty claim policy (M8–M18). */
|
|
56
|
+
claimPolicy?: VtxoClaimPolicy;
|
|
57
|
+
/** V1: independently executable exit package delivered to the owner. */
|
|
58
|
+
exitEntitlement?: VtxoExitEntitlement;
|
|
53
59
|
/** Set when an exit draft consumed this VTXO — prevents double-exit. */
|
|
54
60
|
exitConsumedAt?: number;
|
|
55
61
|
exitReceiptId?: string;
|
|
@@ -99,6 +105,8 @@ export interface TransferVtxoParams {
|
|
|
99
105
|
amount: bigint;
|
|
100
106
|
nonce: string;
|
|
101
107
|
changeNonce?: string;
|
|
108
|
+
/** V1: exit package the recipient must receive with the claim. */
|
|
109
|
+
exitEntitlement?: VtxoExitEntitlement;
|
|
102
110
|
}
|
|
103
111
|
export interface SplitVtxoParams {
|
|
104
112
|
amounts: bigint[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.distributeYield = distributeYield;
|
|
4
|
+
/** Deterministic pro-rata yield distribution for multiparty VTXO/pool claims. */
|
|
5
|
+
function distributeYield(total, weights) {
|
|
6
|
+
if (total < 0n)
|
|
7
|
+
throw new Error('yield total must be non-negative');
|
|
8
|
+
const entries = Object.entries(weights).filter(([, weight]) => weight > 0n);
|
|
9
|
+
if (entries.length === 0)
|
|
10
|
+
throw new Error('yield requires positive weights');
|
|
11
|
+
const denominator = entries.reduce((sum, [, weight]) => sum + weight, 0n);
|
|
12
|
+
if (denominator <= 0n)
|
|
13
|
+
throw new Error('yield weight sum must be positive');
|
|
14
|
+
const out = {};
|
|
15
|
+
let allocated = 0n;
|
|
16
|
+
for (const [id, weight] of entries) {
|
|
17
|
+
const share = (total * weight) / denominator;
|
|
18
|
+
out[id] = share;
|
|
19
|
+
allocated += share;
|
|
20
|
+
}
|
|
21
|
+
// Deterministic remainder assignment to the first entry.
|
|
22
|
+
const remainder = total - allocated;
|
|
23
|
+
if (remainder > 0n)
|
|
24
|
+
out[entries[0][0]] += remainder;
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in VTXO claim policies (M8–M10).
|
|
3
|
+
*
|
|
4
|
+
* The base OmniaVtxo is single-owner. These policies let a VTXO carry a
|
|
5
|
+
* threshold, conditional (HTLC-style) or DvP-bound claim rule. Signatures are
|
|
6
|
+
* assumed to have been cryptographically verified by the caller; this module
|
|
7
|
+
* enforces the authorization *policy* (who/what may claim, and when).
|
|
8
|
+
*/
|
|
9
|
+
import type { OmniaVtxo } from './types.js';
|
|
10
|
+
export interface VtxoPolicyBinding {
|
|
11
|
+
poolId?: string;
|
|
12
|
+
tokenId?: string;
|
|
13
|
+
}
|
|
14
|
+
export type VtxoClaimPolicy = ({
|
|
15
|
+
type: 'single';
|
|
16
|
+
owner: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'threshold';
|
|
19
|
+
threshold: number;
|
|
20
|
+
owners: string[];
|
|
21
|
+
} | {
|
|
22
|
+
type: 'conditional';
|
|
23
|
+
owner: string;
|
|
24
|
+
refundOwner: string;
|
|
25
|
+
hashlock: string;
|
|
26
|
+
timeoutBlock: number;
|
|
27
|
+
} | {
|
|
28
|
+
type: 'dvp';
|
|
29
|
+
owner: string;
|
|
30
|
+
counterparty: string;
|
|
31
|
+
paymentId: string;
|
|
32
|
+
hashlock: string;
|
|
33
|
+
timeoutBlock: number;
|
|
34
|
+
} | {
|
|
35
|
+
type: 'delegated';
|
|
36
|
+
owner: string;
|
|
37
|
+
delegate: string;
|
|
38
|
+
scope: 'claim' | 'transfer';
|
|
39
|
+
expiresAtBlock: number;
|
|
40
|
+
revoked?: boolean;
|
|
41
|
+
} | {
|
|
42
|
+
type: 'rotation';
|
|
43
|
+
fromOperator: string;
|
|
44
|
+
toOperator: string;
|
|
45
|
+
rotateAtBlock: number;
|
|
46
|
+
} | {
|
|
47
|
+
type: 'vesting';
|
|
48
|
+
owner: string;
|
|
49
|
+
beneficiary: string;
|
|
50
|
+
startBlock: number;
|
|
51
|
+
cliffBlock: number;
|
|
52
|
+
endBlock: number;
|
|
53
|
+
} | {
|
|
54
|
+
type: 'recovery';
|
|
55
|
+
owner: string;
|
|
56
|
+
guardians: string[];
|
|
57
|
+
threshold: number;
|
|
58
|
+
recoverAfterBlock: number;
|
|
59
|
+
} | {
|
|
60
|
+
type: 'collateral';
|
|
61
|
+
owner: string;
|
|
62
|
+
counterparty: string;
|
|
63
|
+
releaseAtBlock?: number;
|
|
64
|
+
}) & {
|
|
65
|
+
binding?: VtxoPolicyBinding;
|
|
66
|
+
};
|
|
67
|
+
export interface VtxoClaimWitness {
|
|
68
|
+
signers: string[];
|
|
69
|
+
preimage?: string;
|
|
70
|
+
/** 'claim' (before timeout) or 'refund' (after timeout). */
|
|
71
|
+
path?: 'claim' | 'refund';
|
|
72
|
+
/** Delegated policy scope being exercised. */
|
|
73
|
+
scope?: 'claim' | 'transfer';
|
|
74
|
+
/** Vesting claim amount. */
|
|
75
|
+
claimAmount?: bigint;
|
|
76
|
+
}
|
|
77
|
+
export interface VtxoClaimContext {
|
|
78
|
+
block: number;
|
|
79
|
+
poolId?: string;
|
|
80
|
+
tokenId?: string;
|
|
81
|
+
htlc?: {
|
|
82
|
+
paymentId: string;
|
|
83
|
+
hashlock: string;
|
|
84
|
+
status: 'pending' | 'fulfilled' | 'timed_out';
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export interface VtxoClaimResult {
|
|
88
|
+
valid: boolean;
|
|
89
|
+
error?: string;
|
|
90
|
+
}
|
|
91
|
+
export declare function verifyClaimPolicy(policy: VtxoClaimPolicy, witness: VtxoClaimWitness, ctx: VtxoClaimContext): VtxoClaimResult;
|
|
92
|
+
/** Resolve the policy attached to a VTXO, defaulting to its single owner. */
|
|
93
|
+
export declare function claimPolicyOf(vtxo: OmniaVtxo): VtxoClaimPolicy;
|
|
94
|
+
export declare function verifyVtxoClaim(vtxo: OmniaVtxo, witness: VtxoClaimWitness, ctx: VtxoClaimContext): VtxoClaimResult;
|