@v3finance/core-sdk 0.2.1 → 0.2.2
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/CHANGELOG.md +30 -0
- package/README.md +2 -1
- package/dist/cjs/generated/operations.js +1 -1
- package/dist/cjs/generated/types.d.ts +13 -7
- package/dist/cjs/generated/types.d.ts.map +1 -1
- package/dist/cjs/resources/approvals.d.ts +22 -0
- package/dist/cjs/resources/approvals.d.ts.map +1 -1
- package/dist/cjs/resources/approvals.js +22 -0
- package/dist/cjs/resources/approvals.js.map +1 -1
- package/dist/cjs/resources/transfers.d.ts +64 -16
- package/dist/cjs/resources/transfers.d.ts.map +1 -1
- package/dist/cjs/resources/transfers.js +64 -16
- package/dist/cjs/resources/transfers.js.map +1 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/generated/operations.js +1 -1
- package/dist/esm/generated/types.d.ts +13 -7
- package/dist/esm/generated/types.d.ts.map +1 -1
- package/dist/esm/resources/approvals.d.ts +22 -0
- package/dist/esm/resources/approvals.d.ts.map +1 -1
- package/dist/esm/resources/approvals.js +22 -0
- package/dist/esm/resources/approvals.js.map +1 -1
- package/dist/esm/resources/transfers.d.ts +64 -16
- package/dist/esm/resources/transfers.d.ts.map +1 -1
- package/dist/esm/resources/transfers.js +64 -16
- package/dist/esm/resources/transfers.js.map +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/docs/concepts.md +2 -1
- package/docs/errors.md +30 -12
- package/package.json +1 -1
- package/src/generated/operations.ts +1 -1
- package/src/generated/types.ts +13 -7
- package/src/resources/approvals.ts +22 -0
- package/src/resources/transfers.ts +64 -16
- package/src/version.ts +1 -1
|
@@ -11,7 +11,10 @@ export interface VoteInput {
|
|
|
11
11
|
* The result of sending is `Gated`, and that is the main thing to understand
|
|
12
12
|
* here. The same `{transaction}` body comes back in three situations:
|
|
13
13
|
*
|
|
14
|
-
* applied (201) — executed, the money left
|
|
14
|
+
* applied (201) — executed, the money left (rarely the status is still
|
|
15
|
+
* `pending` with a `txHash`: it went out, but its
|
|
16
|
+
* accounting was not recorded, and an administrator's
|
|
17
|
+
* reconciliation finishes it — do not send it again);
|
|
15
18
|
* pending (202) — awaiting signatures, funds RESERVED but not sent;
|
|
16
19
|
* replayed (200) — a transfer with this `idempotencyKey` already existed,
|
|
17
20
|
* and this is that transfer as it stands NOW.
|
|
@@ -24,7 +27,10 @@ export interface VoteInput {
|
|
|
24
27
|
*
|
|
25
28
|
* confirmed, submitted — it left;
|
|
26
29
|
* awaiting_approval — waiting for signatures, funds reserved;
|
|
27
|
-
* pending — still executing
|
|
30
|
+
* pending — still executing, or its outcome is unknown
|
|
31
|
+
* (the first answer was 502
|
|
32
|
+
* `TRANSFER_OUTCOME_UNKNOWN`) until an
|
|
33
|
+
* administrator reconciles it;
|
|
28
34
|
* failed, rejected,
|
|
29
35
|
* cancelled — it did not leave.
|
|
30
36
|
*/
|
|
@@ -37,6 +43,12 @@ export declare class TransfersResource {
|
|
|
37
43
|
* On `outcome: 'replayed'` read `data.transaction.status` before telling
|
|
38
44
|
* anyone anything — a replay of an attempt that failed, or that is still
|
|
39
45
|
* waiting for signatures, comes back the same way as one that went out.
|
|
46
|
+
*
|
|
47
|
+
* A thrown 502 `TRANSFER_OUTCOME_UNKNOWN` is not a failure: the signing
|
|
48
|
+
* service gave no definite answer, the transaction may have left, and the
|
|
49
|
+
* transfer stays `pending` with its funds reserved (`details.transactionId`).
|
|
50
|
+
* Retry it only with the SAME key — that returns the transfer without
|
|
51
|
+
* executing it again; a new key can send the money twice.
|
|
40
52
|
*/
|
|
41
53
|
send(input: CreateTransferBody, options?: {
|
|
42
54
|
signal?: AbortSignal;
|
|
@@ -45,7 +57,9 @@ export declare class TransfersResource {
|
|
|
45
57
|
/**
|
|
46
58
|
* One transfer in full. Note the shape: this is the transaction itself, not
|
|
47
59
|
* the `{ transaction }` wrapper that `send()` returns — and it carries more,
|
|
48
|
-
* including `amlLevel`, `approvers`, `usdRate` and `failureReason
|
|
60
|
+
* including `amlLevel`, `approvers`, `usdRate` and `failureReason` — the
|
|
61
|
+
* last only on `failed`, `rejected` and `cancelled`, `null` on any other
|
|
62
|
+
* status.
|
|
49
63
|
*/
|
|
50
64
|
get(transferId: string): Promise<TransactionDetail>;
|
|
51
65
|
/**
|
|
@@ -61,11 +75,30 @@ export declare class TransfersResource {
|
|
|
61
75
|
* awaiting_approval — counted, more signatures still needed;
|
|
62
76
|
* rejected — a `reject` is a veto, and the reserve is released;
|
|
63
77
|
* confirmed/failed — this vote completed the threshold, so execution ran
|
|
64
|
-
* INSIDE this request (gas, then broadcast, ~10s)
|
|
78
|
+
* INSIDE this request (gas, then broadcast, ~10s);
|
|
79
|
+
* pending — with no `executionError`: the outcome is NOT
|
|
80
|
+
* established by this answer. The signing service
|
|
81
|
+
* gave no definite answer (the transaction may have
|
|
82
|
+
* left; an administrator's reconciliation settles
|
|
83
|
+
* it), another call is executing the transfer, or
|
|
84
|
+
* the execution broke off. Re-read the transfer:
|
|
85
|
+
* `confirmed`/`failed` is the outcome, `pending` for
|
|
86
|
+
* over 10 minutes by `updatedAt` is waiting for
|
|
87
|
+
* reconciliation. Never send it again.
|
|
65
88
|
*
|
|
66
89
|
* `failed` means the money is back: execution broke after approval and the
|
|
67
90
|
* reserve was released, with the reason in `executionError`. Treating the
|
|
68
|
-
* call as "approved, done" hides both the success and that failure.
|
|
91
|
+
* call as "approved, done" hides both the success and that failure. The
|
|
92
|
+
* exception is a `failed` transfer that has a `txHash`: it went out after
|
|
93
|
+
* reconciliation had already returned the reserve, and an administrator has
|
|
94
|
+
* to correct the ledger. `executionError` beside `rejected` or `cancelled`
|
|
95
|
+
* means the transfer was no longer waiting for signatures when approved,
|
|
96
|
+
* and nothing was executed.
|
|
97
|
+
*
|
|
98
|
+
* Repeating the same decision while the transfer is still waiting finishes
|
|
99
|
+
* what an earlier call left undone (it recorded the vote and broke off) — a
|
|
100
|
+
* veto, or execution once the threshold is met — instead of 409
|
|
101
|
+
* `ALREADY_VOTED`.
|
|
69
102
|
*/
|
|
70
103
|
vote(transferId: string, input: VoteInput): Promise<VoteResult>;
|
|
71
104
|
/**
|
|
@@ -84,19 +117,34 @@ export declare class TransfersResource {
|
|
|
84
117
|
*/
|
|
85
118
|
cancel(transferId: string): Promise<CancelResult>;
|
|
86
119
|
/**
|
|
87
|
-
* Settles a transfer
|
|
88
|
-
*
|
|
120
|
+
* Settles a transfer nothing else can close. Administrators only.
|
|
121
|
+
*
|
|
122
|
+
* pending — execution died, or its outcome is unknown (the
|
|
123
|
+
* send answered 502 `TRANSFER_OUTCOME_UNKNOWN`);
|
|
124
|
+
* awaiting_approval — with no live approval request to vote on;
|
|
125
|
+
* failed, rejected,
|
|
126
|
+
* cancelled — closed, but the reserve is still held: it is
|
|
127
|
+
* returned, and the status stays.
|
|
128
|
+
*
|
|
129
|
+
* A pending transfer is decided by the HASH: send `txHash` and it is driven
|
|
130
|
+
* to `confirmed`; send none and the reserve is returned (`failed`). Sending
|
|
131
|
+
* none is YOUR statement that nothing was broadcast — the server does not
|
|
132
|
+
* check it, so look on the chain first: returning the reserve of a
|
|
133
|
+
* transaction that did go out creates money. If the transfer already holds
|
|
134
|
+
* a hash from the signing service, only that hash, sent explicitly, settles
|
|
135
|
+
* it — 409 `TRANSFER_BROADCAST` without one, `TXHASH_MISMATCH` with another,
|
|
136
|
+
* the saved hash in `details.txHash`. The other two cases never executed
|
|
137
|
+
* and take no hash.
|
|
89
138
|
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* broadcast. A FRESH pending transfer is refused on purpose — while
|
|
94
|
-
* execution may still be in flight, intervening risks a SECOND broadcast,
|
|
95
|
-
* which costs more than waiting.
|
|
139
|
+
* A FRESH transfer is refused on purpose — while execution may still be in
|
|
140
|
+
* flight, intervening risks a SECOND broadcast, which costs more than
|
|
141
|
+
* waiting.
|
|
96
142
|
*
|
|
97
|
-
* Pass `fee` along with the hash if you know it: this is the only moment
|
|
98
|
-
*
|
|
99
|
-
*
|
|
143
|
+
* Pass `fee` along with the hash if you know it: this is the only moment it
|
|
144
|
+
* can be recorded on a transfer settled this way; otherwise it stays as it
|
|
145
|
+
* is, null unless the signing service reported one. The answer says which
|
|
146
|
+
* way it went — `confirmed`, `failed`, or the unchanged `rejected` /
|
|
147
|
+
* `cancelled` of a closed transfer.
|
|
100
148
|
*/
|
|
101
149
|
reconcile(transferId: string, input?: ReconcileTransferBody): Promise<ReconcileTransferResult>;
|
|
102
150
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transfers.d.ts","sourceRoot":"","sources":["../../../src/resources/transfers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAA;AACtD,OAAO,EAAW,KAAK,KAAK,EAAE,MAAM,kBAAkB,CAAA;AACtD,OAAO,KAAK,EACV,YAAY,EACZ,uBAAuB,EACvB,kBAAkB,EAClB,qBAAqB,EACrB,uBAAuB,EACvB,iBAAiB,EACjB,UAAU,EACX,MAAM,uBAAuB,CAAA;AAE9B,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,SAAS,GAAG,QAAQ,CAAA;IAC9B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC7B;AAED
|
|
1
|
+
{"version":3,"file":"transfers.d.ts","sourceRoot":"","sources":["../../../src/resources/transfers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAO,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAA;AACtD,OAAO,EAAW,KAAK,KAAK,EAAE,MAAM,kBAAkB,CAAA;AACtD,OAAO,KAAK,EACV,YAAY,EACZ,uBAAuB,EACvB,kBAAkB,EAClB,qBAAqB,EACrB,uBAAuB,EACvB,iBAAiB,EACjB,UAAU,EACX,MAAM,uBAAuB,CAAA;AAE9B,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,SAAS,GAAG,QAAQ,CAAA;IAC9B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,qBAAa,iBAAiB;;IAI5B,YAAY,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAG9C;IAED;;;;;;;;;;;;OAYG;IACG,IAAI,CACR,KAAK,EAAE,kBAAkB,EACzB,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAO,GACzD,OAAO,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC,CAUzC;IAED;;;;;;OAMG;IACH,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAElD;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAqCG;IACH,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAM9D;IAED;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAEhD;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,SAAS,CACP,UAAU,EAAE,MAAM,EAClB,KAAK,GAAE,qBAA0B,GAChC,OAAO,CAAC,uBAAuB,CAAC,CAMlC;CACF"}
|
|
@@ -6,7 +6,10 @@ import { toGated } from '../core/gated.js';
|
|
|
6
6
|
* The result of sending is `Gated`, and that is the main thing to understand
|
|
7
7
|
* here. The same `{transaction}` body comes back in three situations:
|
|
8
8
|
*
|
|
9
|
-
* applied (201) — executed, the money left
|
|
9
|
+
* applied (201) — executed, the money left (rarely the status is still
|
|
10
|
+
* `pending` with a `txHash`: it went out, but its
|
|
11
|
+
* accounting was not recorded, and an administrator's
|
|
12
|
+
* reconciliation finishes it — do not send it again);
|
|
10
13
|
* pending (202) — awaiting signatures, funds RESERVED but not sent;
|
|
11
14
|
* replayed (200) — a transfer with this `idempotencyKey` already existed,
|
|
12
15
|
* and this is that transfer as it stands NOW.
|
|
@@ -19,7 +22,10 @@ import { toGated } from '../core/gated.js';
|
|
|
19
22
|
*
|
|
20
23
|
* confirmed, submitted — it left;
|
|
21
24
|
* awaiting_approval — waiting for signatures, funds reserved;
|
|
22
|
-
* pending — still executing
|
|
25
|
+
* pending — still executing, or its outcome is unknown
|
|
26
|
+
* (the first answer was 502
|
|
27
|
+
* `TRANSFER_OUTCOME_UNKNOWN`) until an
|
|
28
|
+
* administrator reconciles it;
|
|
23
29
|
* failed, rejected,
|
|
24
30
|
* cancelled — it did not leave.
|
|
25
31
|
*/
|
|
@@ -36,6 +42,12 @@ export class TransfersResource {
|
|
|
36
42
|
* On `outcome: 'replayed'` read `data.transaction.status` before telling
|
|
37
43
|
* anyone anything — a replay of an attempt that failed, or that is still
|
|
38
44
|
* waiting for signatures, comes back the same way as one that went out.
|
|
45
|
+
*
|
|
46
|
+
* A thrown 502 `TRANSFER_OUTCOME_UNKNOWN` is not a failure: the signing
|
|
47
|
+
* service gave no definite answer, the transaction may have left, and the
|
|
48
|
+
* transfer stays `pending` with its funds reserved (`details.transactionId`).
|
|
49
|
+
* Retry it only with the SAME key — that returns the transfer without
|
|
50
|
+
* executing it again; a new key can send the money twice.
|
|
39
51
|
*/
|
|
40
52
|
async send(input, options = {}) {
|
|
41
53
|
const res = await this.#http.requestRaw('POST', this.#base, {
|
|
@@ -51,7 +63,9 @@ export class TransfersResource {
|
|
|
51
63
|
/**
|
|
52
64
|
* One transfer in full. Note the shape: this is the transaction itself, not
|
|
53
65
|
* the `{ transaction }` wrapper that `send()` returns — and it carries more,
|
|
54
|
-
* including `amlLevel`, `approvers`, `usdRate` and `failureReason
|
|
66
|
+
* including `amlLevel`, `approvers`, `usdRate` and `failureReason` — the
|
|
67
|
+
* last only on `failed`, `rejected` and `cancelled`, `null` on any other
|
|
68
|
+
* status.
|
|
55
69
|
*/
|
|
56
70
|
get(transferId) {
|
|
57
71
|
return this.#http.request('GET', `${this.#base}/${seg(transferId)}`);
|
|
@@ -69,11 +83,30 @@ export class TransfersResource {
|
|
|
69
83
|
* awaiting_approval — counted, more signatures still needed;
|
|
70
84
|
* rejected — a `reject` is a veto, and the reserve is released;
|
|
71
85
|
* confirmed/failed — this vote completed the threshold, so execution ran
|
|
72
|
-
* INSIDE this request (gas, then broadcast, ~10s)
|
|
86
|
+
* INSIDE this request (gas, then broadcast, ~10s);
|
|
87
|
+
* pending — with no `executionError`: the outcome is NOT
|
|
88
|
+
* established by this answer. The signing service
|
|
89
|
+
* gave no definite answer (the transaction may have
|
|
90
|
+
* left; an administrator's reconciliation settles
|
|
91
|
+
* it), another call is executing the transfer, or
|
|
92
|
+
* the execution broke off. Re-read the transfer:
|
|
93
|
+
* `confirmed`/`failed` is the outcome, `pending` for
|
|
94
|
+
* over 10 minutes by `updatedAt` is waiting for
|
|
95
|
+
* reconciliation. Never send it again.
|
|
73
96
|
*
|
|
74
97
|
* `failed` means the money is back: execution broke after approval and the
|
|
75
98
|
* reserve was released, with the reason in `executionError`. Treating the
|
|
76
|
-
* call as "approved, done" hides both the success and that failure.
|
|
99
|
+
* call as "approved, done" hides both the success and that failure. The
|
|
100
|
+
* exception is a `failed` transfer that has a `txHash`: it went out after
|
|
101
|
+
* reconciliation had already returned the reserve, and an administrator has
|
|
102
|
+
* to correct the ledger. `executionError` beside `rejected` or `cancelled`
|
|
103
|
+
* means the transfer was no longer waiting for signatures when approved,
|
|
104
|
+
* and nothing was executed.
|
|
105
|
+
*
|
|
106
|
+
* Repeating the same decision while the transfer is still waiting finishes
|
|
107
|
+
* what an earlier call left undone (it recorded the vote and broke off) — a
|
|
108
|
+
* veto, or execution once the threshold is met — instead of 409
|
|
109
|
+
* `ALREADY_VOTED`.
|
|
77
110
|
*/
|
|
78
111
|
vote(transferId, input) {
|
|
79
112
|
return this.#http.request('POST', `${this.#base}/${seg(transferId)}/approvals`, { body: input });
|
|
@@ -96,19 +129,34 @@ export class TransfersResource {
|
|
|
96
129
|
return this.#http.request('POST', `${this.#base}/${seg(transferId)}/cancel`);
|
|
97
130
|
}
|
|
98
131
|
/**
|
|
99
|
-
* Settles a transfer
|
|
100
|
-
*
|
|
132
|
+
* Settles a transfer nothing else can close. Administrators only.
|
|
133
|
+
*
|
|
134
|
+
* pending — execution died, or its outcome is unknown (the
|
|
135
|
+
* send answered 502 `TRANSFER_OUTCOME_UNKNOWN`);
|
|
136
|
+
* awaiting_approval — with no live approval request to vote on;
|
|
137
|
+
* failed, rejected,
|
|
138
|
+
* cancelled — closed, but the reserve is still held: it is
|
|
139
|
+
* returned, and the status stays.
|
|
140
|
+
*
|
|
141
|
+
* A pending transfer is decided by the HASH: send `txHash` and it is driven
|
|
142
|
+
* to `confirmed`; send none and the reserve is returned (`failed`). Sending
|
|
143
|
+
* none is YOUR statement that nothing was broadcast — the server does not
|
|
144
|
+
* check it, so look on the chain first: returning the reserve of a
|
|
145
|
+
* transaction that did go out creates money. If the transfer already holds
|
|
146
|
+
* a hash from the signing service, only that hash, sent explicitly, settles
|
|
147
|
+
* it — 409 `TRANSFER_BROADCAST` without one, `TXHASH_MISMATCH` with another,
|
|
148
|
+
* the saved hash in `details.txHash`. The other two cases never executed
|
|
149
|
+
* and take no hash.
|
|
101
150
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* broadcast. A FRESH pending transfer is refused on purpose — while
|
|
106
|
-
* execution may still be in flight, intervening risks a SECOND broadcast,
|
|
107
|
-
* which costs more than waiting.
|
|
151
|
+
* A FRESH transfer is refused on purpose — while execution may still be in
|
|
152
|
+
* flight, intervening risks a SECOND broadcast, which costs more than
|
|
153
|
+
* waiting.
|
|
108
154
|
*
|
|
109
|
-
* Pass `fee` along with the hash if you know it: this is the only moment
|
|
110
|
-
*
|
|
111
|
-
*
|
|
155
|
+
* Pass `fee` along with the hash if you know it: this is the only moment it
|
|
156
|
+
* can be recorded on a transfer settled this way; otherwise it stays as it
|
|
157
|
+
* is, null unless the signing service reported one. The answer says which
|
|
158
|
+
* way it went — `confirmed`, `failed`, or the unchanged `rejected` /
|
|
159
|
+
* `cancelled` of a closed transfer.
|
|
112
160
|
*/
|
|
113
161
|
reconcile(transferId, input = {}) {
|
|
114
162
|
return this.#http.request('POST', `${this.#base}/${seg(transferId)}/reconcile`, { body: input });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transfers.js","sourceRoot":"","sources":["../../../src/resources/transfers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAmB,MAAM,iBAAiB,CAAA;AACtD,OAAO,EAAE,OAAO,EAAc,MAAM,kBAAkB,CAAA;AAgBtD
|
|
1
|
+
{"version":3,"file":"transfers.js","sourceRoot":"","sources":["../../../src/resources/transfers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAmB,MAAM,iBAAiB,CAAA;AACtD,OAAO,EAAE,OAAO,EAAc,MAAM,kBAAkB,CAAA;AAgBtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,OAAO,iBAAiB;IACnB,KAAK,CAAY;IACjB,KAAK,CAAQ;IAEtB,YAAY,IAAgB,EAAE,SAAiB;QAC7C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;QACjB,IAAI,CAAC,KAAK,GAAG,kBAAkB,GAAG,CAAC,SAAS,CAAC,YAAY,CAAA;IAC3D,CAAC;IAED;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,IAAI,CACR,KAAyB,EACzB,OAAO,GAAiD,EAAE;QAE1D,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAA0B,MAAM,EAAE,IAAI,CAAC,KAAK,EAAE;YACnF,IAAI,EAAE,KAAK;YACX,GAAG,CAAC,KAAK,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,KAAK,CAAC,cAAc,EAAE,CAAC;YACvF,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;YACnE,GAAG,CAAC,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC;SAC7E,CAAC,CAAA;QACF,OAAO,OAAO,CAAmD,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,EAAE;YACrF,WAAW,EAAE,IAAI;SAClB,CAAC,CAAA;IACJ,CAAC;IAED;;;;;;OAMG;IACH,GAAG,CAAC,UAAkB;QACpB,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAoB,KAAK,EAAE,GAAG,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,CAAA;IACzF,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAqCG;IACH,IAAI,CAAC,UAAkB,EAAE,KAAgB;QACvC,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CACvB,MAAM,EACN,GAAG,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,UAAU,CAAC,YAAY,EAC5C,EAAE,IAAI,EAAE,KAAK,EAAE,CAChB,CAAA;IACH,CAAC;IAED;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,UAAkB;QACvB,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAe,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,CAAA;IAC5F,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,SAAS,CACP,UAAkB,EAClB,KAAK,GAA0B,EAAE;QAEjC,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CACvB,MAAM,EACN,GAAG,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,UAAU,CAAC,YAAY,EAC5C,EAAE,IAAI,EAAE,KAAK,EAAE,CAChB,CAAA;IACH,CAAC;CACF"}
|
package/dist/esm/version.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** SDK version, sent in the `user-agent` header. */
|
|
2
|
-
export declare const SDK_VERSION = "0.2.
|
|
2
|
+
export declare const SDK_VERSION = "0.2.2";
|
|
3
3
|
/** Default API base URL. */
|
|
4
4
|
export declare const DEFAULT_BASE_URL = "https://policy-engine-rest.ezig.workers.dev";
|
|
5
5
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/esm/version.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** SDK version, sent in the `user-agent` header. */
|
|
2
|
-
export const SDK_VERSION = '0.2.
|
|
2
|
+
export const SDK_VERSION = '0.2.2';
|
|
3
3
|
/** Default API base URL. */
|
|
4
4
|
export const DEFAULT_BASE_URL = 'https://policy-engine-rest.ezig.workers.dev';
|
|
5
5
|
//# sourceMappingURL=version.js.map
|
package/docs/concepts.md
CHANGED
|
@@ -73,7 +73,8 @@ switch (result.outcome) {
|
|
|
73
73
|
server and only the answer was lost — not that it succeeded. The body is that transfer as it stands
|
|
74
74
|
now, so `replayed` must not be shown as "sent": `data.transaction.status` says whether it went out
|
|
75
75
|
(`confirmed`, or `submitted`), is waiting for signatures (`awaiting_approval`), is still executing
|
|
76
|
-
(`pending`) or did not go out (`failed`, `rejected`,
|
|
76
|
+
or waiting for an administrator to reconcile it (`pending`) or did not go out (`failed`, `rejected`,
|
|
77
|
+
`cancelled`).
|
|
77
78
|
|
|
78
79
|
**Only `transfers.send` can return it.** `idempotencyKey` exists on exactly one request body in the
|
|
79
80
|
whole API — the transfer — so it is the only place where a 200 means "this already happened". On
|
package/docs/errors.md
CHANGED
|
@@ -87,7 +87,10 @@ that status so that one condition has one name whichever side produced it.
|
|
|
87
87
|
502 and 504 are the exception, and they are the two statuses to **branch on by
|
|
88
88
|
`status` rather than by `code`**: the API has no envelope for them — its own
|
|
89
89
|
derivation would call both `INTERNAL_ERROR` — they arrive from the edge as HTML,
|
|
90
|
-
and the SDK is the only side naming them.
|
|
90
|
+
and the SDK is the only side naming them. A 502 that carries a code of its own
|
|
91
|
+
came from the API and is not this case: on `POST /transfers` it can be
|
|
92
|
+
`TRANSFER_OUTCOME_UNKNOWN`, which must not be treated as a failure. Check `code`
|
|
93
|
+
before `status`.
|
|
91
94
|
|
|
92
95
|
One asymmetry left on the server side, worth knowing only so you read it
|
|
93
96
|
correctly: the API's envelope would derive `UNAUTHORIZED` for a 401 whose body
|
|
@@ -102,6 +105,7 @@ the authentication middleware with `UNAUTHENTICATED`.
|
|
|
102
105
|
|---|---|---|---|
|
|
103
106
|
| `INSUFFICIENT_BALANCE` | 400 | The atomic reserve failed: the address does not have that much of that token available. A concurrent transfer may have taken it. | Show the shortfall. Refresh balances before offering a retry — the amount that fits may have changed. Do not retry blind. |
|
|
104
107
|
| `INVALID_AMOUNT` | 400 | Amount is not a positive decimal string, or has more decimals than the token allows. | Fix client-side. A user-facing validation message, not a retry. |
|
|
108
|
+
| `INVALID_DESTINATION_ADDRESS` | 400 | `toAddress` is not a valid address on the sending address's network: TRON — base58check (34 characters, `T…`, checksum); EVM — `0x` + 40 hex, with the EIP-55 checksum when mixed-case. The string is checked as sent, so leading or trailing whitespace fails too. Checked before AML, the policy and the reserve — nothing was created or reserved. `details` is `{field: "toAddress", network}`. A network of a type the API does not know is not checked here. | Fix client-side: an error on the recipient field. Not a retry. |
|
|
105
109
|
| `TOKEN_NOT_FOUND` | 400 | The asset is not in the registry, or is disabled (kill-switched during an incident). | Refresh the token registry. If it is still missing, the asset is unavailable — say so; do not retry. |
|
|
106
110
|
| `TOKEN_NETWORK_MISMATCH` | 400 | The token lives on a different network than the sending address. | A client bug or a stale form. Re-resolve the token for the address's network. |
|
|
107
111
|
| `ADDRESS_NOT_FOUND` | 404 | No such address id. | Refresh the address list; the address was deleted or the id is stale. |
|
|
@@ -109,10 +113,13 @@ the authentication middleware with `UNAUTHENTICATED`.
|
|
|
109
113
|
| `ADDRESS_TRANSFER_FORBIDDEN` | 403 | You are neither the owner nor an admin, and you hold no grant with transfer rights on this address. Checked before the policy engine. | Show the address as view-only. The remedy is a `contributor` grant from the owner — direct the user there, not to a retry. |
|
|
110
114
|
| `POLICY_DENIED` | 403 | The policy engine said `block`, or no policy permits the action (default-deny). `details` carries the full decision, including `trace`. | Terminal for this request. Show `message` — it already names the rule that fired. Changing the amount or the destination may make the same request pass; retrying it unchanged will not. |
|
|
111
115
|
| `APPROVAL_UNSATISFIABLE` | 409 | Signatures are required but cannot be gathered: fewer eligible approvers than the threshold. `details` has `{threshold, eligibleApprovers}`. | **Do not retry.** Nobody can approve this. Tell the user to contact an administrator — the policy or the approver group has to change. See [Easily confused](#easily-confused). |
|
|
116
|
+
| `IDEMPOTENCY_KEY_REUSED` | 409 | The `idempotencyKey` already belongs to a different transfer: another sending address, recipient, amount, asset or network — their names are in `details.fields` (the amount is compared by value, `1.5` equals `1.50`); or it was used by another person or in another vault, and then there is no `details`. Nothing was created or moved. | Not a replay — a key reused for a new attempt. A new transfer needs a new key; a retry of the same transfer needs the old key with the same body. |
|
|
112
117
|
| `APPROVAL_FAILED` | 409 | The approval request could not be created for some other reason. The reserve has already been returned, so no funds are stuck. | Retry once with a **new** idempotency key. If it repeats, it is a server-side fault — escalate. |
|
|
113
118
|
| `VAULT_HAS_NO_WALLET_SERVICE` | 500 | The vault has no `walletServiceUrl` configured. | Configuration fault, not a user error. Transfers from this vault cannot work at all. Contact an administrator. |
|
|
114
119
|
| `GAS_STATION_FAILED` | 502 | Energy/bandwidth provisioning failed. The transfer is marked `failed` and the reserve is already returned. | Retry with a **new** idempotency key (the old one is bound to the failed row). If it persists, an upstream service is down. |
|
|
115
|
-
| `WALLET_SERVICE_ERROR` |
|
|
120
|
+
| `WALLET_SERVICE_ERROR` | 400 | The signing service refused the request (answered 4xx). A definite refusal: the transfer is `failed` and the reserve is returned. Every other failure of the signing service is `TRANSFER_OUTCOME_UNKNOWN`, not this. | A retry will fail identically. `message` gives only the service's status (`Wallet Service returned 400`): its own body is not echoed to clients, it goes to the worker log. Show that the signing service refused and report it; do not try to explain the refusal from `message`. A new attempt needs a new key. |
|
|
121
|
+
| `TRANSFER_OUTCOME_UNKNOWN` | 502 | The signing service gave no definite answer — none within 60 s, a broken connection, a 5xx or 524, or a 2xx without a transaction id — so the transaction may or may not have reached the network. The transfer stays `pending` with the funds reserved; `details` is `{transactionId, status: "pending"}`. An administrator's reconciliation settles it. | **Neither a refusal nor a success.** Show that the outcome is being established. **Never retry with a new key** — that can send the money twice. A retry with the same key answers 200 (`replayed`) with the transfer as it stands and does not execute it again. Follow the transfer by `details.transactionId`. |
|
|
122
|
+
| `TRANSFER_BROADCAST_UNRECORDED` | 502 | The transaction went to the network (`details.txHash`), but by then reconciliation had already returned its reserve, so the send is not recorded in the ledger. | **The money left — do not retry.** An administrator has to post a correcting entry; report the hash. |
|
|
116
123
|
| `TRANSFER_NOT_EXECUTABLE` | 404 / 409 / 500 | The transfer is missing, or is not in the status execution requires. | Re-read the transfer and drive the UI from its real status. Do not retry. |
|
|
117
124
|
|
|
118
125
|
### Voting on a transfer
|
|
@@ -124,24 +131,33 @@ the authentication middleware with `UNAUTHENTICATED`.
|
|
|
124
131
|
| `TRANSFER_NOT_FOUND` | 404 | No such transfer, or an `awaiting_approval` transfer has no approval request — which is a data inconsistency, not a normal state. | Refresh the list. If a transfer you can see keeps producing this, report it. |
|
|
125
132
|
| `NOT_AWAITING_APPROVAL` | 409 | Voting is over: someone else's vote already resolved it, or it was cancelled. | Not an error worth an alert. Refresh the transfer and show its real status. |
|
|
126
133
|
| `NOT_AN_APPROVER` | 403 | You are not in the approver snapshot taken when the request was created. Editing the policy or the group afterwards does not add you. | Hide the approve/reject controls for this item. Terminal. |
|
|
127
|
-
| `ALREADY_VOTED` | 409 | You already voted. One vote per person. | Refresh; show your recorded vote. |
|
|
134
|
+
| `ALREADY_VOTED` | 409 | You already voted. One vote per person. Repeating the **same** decision is not refused when it can finish what an earlier call left undone (it recorded the vote and broke off): a repeated reject completes the veto, a repeated approve executes the transfer once the threshold is met. A different decision over a recorded vote is always this 409. | Refresh; show your recorded vote. |
|
|
135
|
+
| `REQUEST_INCOMPLETE` | 409 | The approval request behind the transfer was not created completely — signature steps it refers to are missing — so it can never be approved. The `approve` that got this 409 **is recorded**. | Do not offer approve again. The request is closed by its initiator or an admin (cancel; anyone else gets 403 `NOT_INITIATOR`), or by a veto from an approver who has not voted yet; the reserve is returned. Then create the transfer anew. |
|
|
128
136
|
| `NOT_INITIATOR` | 403 | Only the initiator or an admin can cancel. | Hide the cancel control. |
|
|
129
137
|
|
|
130
138
|
### Reconciling a stuck transfer
|
|
131
139
|
|
|
132
|
-
`POST /transfers/{id}/reconcile`, admin only. For a transfer
|
|
140
|
+
`POST /transfers/{id}/reconcile`, admin only. For a transfer nothing else can close: `pending` because execution died or its outcome is unknown (`TRANSFER_OUTCOME_UNKNOWN`); `awaiting_approval` with no live (`PENDING`) approval request; and a closed transfer — `failed`, `rejected` or `cancelled` — whose reserve is still held, where the reserve is returned, the status stays, and the answer carries that status.
|
|
141
|
+
|
|
142
|
+
A `pending` transfer is decided by the hash: with `txHash` it is driven to `confirmed`; without one the reserve is returned (`failed`) — that is the reconciler's statement that nothing was broadcast, not something the server checks. So for a `pending` transfer with no hash, look on the chain from the sending address first: returning the reserve of a transaction that did go out creates money. A `pending` transfer that already has a hash saved from the signing service is settled only by that hash, sent explicitly. The other two cases never executed: they take no hash, and the reserve is returned.
|
|
133
143
|
|
|
134
144
|
| Code | Status | When | What to do |
|
|
135
145
|
|---|---|---|---|
|
|
136
|
-
| `TRANSFER_STILL_EXECUTING` | 409 | The transfer
|
|
137
|
-
| `TRANSFER_NOT_STUCK` | 409 |
|
|
138
|
-
| `RESERVE_ALREADY_CONSUMED` | 409 |
|
|
146
|
+
| `TRANSFER_STILL_EXECUTING` | 409 | The transfer changed less than 10 minutes ago (`updatedAt`). An unknown outcome counts as a change: the clock starts when execution stopped waiting for the signing service. | **This is "wait", not "no".** Execution may still be in flight; intervening now risks a second broadcast. Back off and offer the action again after the window. See [Easily confused](#easily-confused). |
|
|
147
|
+
| `TRANSFER_NOT_STUCK` | 409 | Nothing to reconcile: the transfer is in none of the states above (an `awaiting_approval` one still has a live request, a closed one holds no reserve), or another path changed it while you were reconciling. | Re-read it. It resolved itself, or it is closed by a vote or a cancel; nothing to do here. |
|
|
148
|
+
| `RESERVE_ALREADY_CONSUMED` | 409 | Without `txHash`: a `confirm` posting has already spent the reserve — the transaction went to the network. With `txHash`: the reserve was returned in parallel, for instance by a reconciliation without a hash. | Without a hash: **never retry as a release.** The money left. Finish the transfer by supplying its `txHash` instead. With a hash: re-read the transfer. |
|
|
149
|
+
| `TRANSFER_BROADCAST` | 409 | No `txHash` sent, but the `pending` transfer has a hash saved from the signing service — it went to the network. `details.txHash` is that hash. Nothing moved. | Reconcile again with that `txHash`. Never as a release. |
|
|
150
|
+
| `TXHASH_MISMATCH` | 409 | The `txHash` differs from the hash saved on the transfer, which is in `details.txHash`. Nothing moved. | Check the hash. The saved one came from the signing service. |
|
|
151
|
+
| `TXHASH_IN_USE` | 409 | The `txHash` is already recorded on another transfer. Nothing moved. | Almost certainly the wrong hash. Check it. |
|
|
152
|
+
| `TRANSFER_NOT_EXECUTED` | 409 | `txHash` sent for a transfer that never executed — `awaiting_approval`, or closed with its reserve still held. | Reconcile without `txHash`; the reserve is returned. |
|
|
153
|
+
| `RESERVE_MISSING` | 409 | `txHash` sent for a transfer whose funds were never reserved, so it cannot have executed. | Reconcile without `txHash`; it closes as `failed` with no money moved. |
|
|
154
|
+
| `RESERVE_RELEASED` | 409 | `txHash` sent, but the reserve had already been returned: on a `pending` transfer the returned funds are already spent and the send cannot be charged to them; a closed transfer is not confirmed by reconciliation at all. Nothing moved. | An administrator has to post a correcting entry. Escalate. |
|
|
139
155
|
|
|
140
156
|
### Not error codes, though they look like them
|
|
141
157
|
|
|
142
158
|
Two sets of uppercase strings travel inside **successful** bodies and will mislead anyone grepping for codes:
|
|
143
159
|
|
|
144
|
-
- **`applyError` on a 200 vote response** — `STALE`, `APPLY_FAILED`, `APPLY_FAILED_ROLLED_BACK`, `APPLY_FAILED_PARTIAL`, `TRANSFER_EXECUTION_FAILED`. The vote itself succeeded and the request reached its final status; what failed was applying the approved change. No exception is thrown, so you must read this field. `TRANSFER_EXECUTION_FAILED` in particular means the approval went through and the transfer
|
|
160
|
+
- **`applyError` on a 200 vote response** — `STALE`, `APPLY_FAILED`, `APPLY_FAILED_ROLLED_BACK`, `APPLY_FAILED_PARTIAL`, `TRANSFER_EXECUTION_FAILED`. The vote itself succeeded and the request reached its final status; what failed was applying the approved change. No exception is thrown, so you must read this field. `TRANSFER_EXECUTION_FAILED` in particular means the approval went through and the transfer was not executed — usually `failed` with the reserve returned; the transfer itself says which. An unknown outcome is **not** reported here: there is no `TRANSFER_OUTCOME_UNKNOWN` in a vote answer. The transfer route answers `status: "pending"` with `executionError: null`, the request route `APPROVED` with `finalized: false` and `applyError: null` — the outcome is not established by that answer; read it from the transfer.
|
|
145
161
|
- **Policy `reason` strings** inside `POLICY_DENIED`'s `details`, and `observations[].code` in report bodies. The first are author-defined — an administrator writes `DAILY_LIMIT_EXCEEDED` or `AMOUNT_EXCEEDED` into a policy by hand, so the set is open and unstable. The second are report findings, not failures. Do not branch on either as if it were an error code.
|
|
146
162
|
|
|
147
163
|
The observation set, unlike the policy reasons, is closed, and it is all nine of these — each with a `severity` of `info` or `warning`, which is the field to drive presentation from:
|
|
@@ -268,7 +284,8 @@ In the single-subject form the per-subject code is returned as a normal error
|
|
|
268
284
|
| `NOT_FOUND` | 404 | No such request in this vault. | Refresh the list. |
|
|
269
285
|
| `NOT_PENDING` | 409 | The request is already `APPROVED` / `REJECTED` / `CANCELLED`. Voting is over. | Refresh and show the final status. Expected under concurrency; not worth an alert. |
|
|
270
286
|
| `NOT_APPROVER` | 403 | You are not in the approver snapshot. The snapshot is frozen at creation — later policy or group edits do not add you. | Hide the controls for this request. Terminal. |
|
|
271
|
-
| `ALREADY_VOTED` | 409 | One vote per person, already cast. | Refresh; show your vote. |
|
|
287
|
+
| `ALREADY_VOTED` | 409 | One vote per person, already cast. Repeating the **same** decision on a `PENDING` request is not refused when it can finish what an earlier call left undone: a repeated reject completes the veto, a repeated approve completes the approval once the threshold is met. | Refresh; show your vote. |
|
|
288
|
+
| `REQUEST_INCOMPLETE` | 409 | The request was not created completely — signature steps its approvers refer to are missing — so it can never be approved. The `approve` that got this 409 **is recorded**. | Do not offer approve again. It is closed by its initiator or an admin (cancel; anyone else gets 403 `NOT_INITIATOR`), or by a veto from an approver who has not voted yet. Then create the action anew. |
|
|
272
289
|
| `NOT_INITIATOR` | 403 | Only the initiator or an admin can cancel. | Hide the cancel control. |
|
|
273
290
|
| `UNSATISFIABLE` | 409 | The request cannot be created: a step has fewer eligible approvers than its threshold, or every step is auto-approved so no request is needed. | **Do not retry.** An administrator has to fix the policy. Same situation as `APPROVAL_UNSATISFIABLE` on a transfer. |
|
|
274
291
|
| `DB_REQUIRED` | 500 | A money-path request was handled without the database binding. Deliberately fail-closed, so no reserve is left locked. | Infrastructure fault. Nothing the user can do; escalate. |
|
|
@@ -413,7 +430,7 @@ Rate limiting outside `/api/auth/*` is not implemented in the API itself. A 429
|
|
|
413
430
|
|
|
414
431
|
**`TRANSFER_STILL_EXECUTING` means "wait", not "no".** The transfer went into `pending` less than 10 minutes ago and execution may still be in flight. The server refuses on purpose: intervening could cause a **second broadcast**, which is far more expensive than waiting. Do not surface it as a failure, do not loop on it — back off past the window and offer the action again.
|
|
415
432
|
|
|
416
|
-
**`RESERVE_ALREADY_CONSUMED` means the money left.** It appears when a release is attempted over a transfer whose reserve was already spent by a `confirm` posting. Idempotency does not protect you here: confirm and release are different keys that draw on the same balance, so a release layered on a completed confirm would create money out of nothing. The correct next step is always to finish the transfer by its `txHash`, never to retry the release.
|
|
433
|
+
**`RESERVE_ALREADY_CONSUMED` means the money left.** It appears when a release is attempted over a transfer whose reserve was already spent by a `confirm` posting. Idempotency does not protect you here: confirm and release are different keys that draw on the same balance, so a release layered on a completed confirm would create money out of nothing. The correct next step is always to finish the transfer by its `txHash`, never to retry the release. (Reconciling **with** a `txHash` can get the same code for the opposite reason — the reserve was returned in parallel; then re-read the transfer.)
|
|
417
434
|
|
|
418
435
|
**`CONCURRENCY_CONFLICT` on address creation is the opposite of the above: retry it.** It means the server could not allocate a unique derivation index because other creations were racing it. Nothing was created, nothing is half-done, and the next attempt will usually succeed. Make this the one 409 in the SDK you retry automatically.
|
|
419
436
|
|
|
@@ -466,11 +483,12 @@ else showByStatus(result.data.transaction.status) // replayed: NOT necessarily s
|
|
|
466
483
|
Rules that follow from how the server handles the key:
|
|
467
484
|
|
|
468
485
|
- **The key is reserved before any external call.** A replay is therefore recognised even if the first attempt died mid-broadcast. The server answers **200** and does not broadcast again; the SDK labels that `outcome: 'replayed'`.
|
|
469
|
-
- **`replayed` does not mean "sent".** It means "a transfer with this key already exists": the earlier attempt did reach the server and only its answer was lost. `data` is that transfer as it stands **now**, and what happened to it is in `data.transaction.status` — `confirmed` (or `submitted`): it left; `awaiting_approval`: it is waiting for signatures, funds reserved; `pending`: it is still executing; `failed`, `rejected` or `cancelled`: it did not leave. Tell the person that, not "sent".
|
|
470
|
-
- **A new key after a `failed` or `rejected` transfer.** The old key is bound to that row, and reusing it returns the dead transfer instead of sending a new one.
|
|
486
|
+
- **`replayed` does not mean "sent".** It means "a transfer with this key already exists": the earlier attempt did reach the server and only its answer was lost. `data` is that transfer as it stands **now**, and what happened to it is in `data.transaction.status` — `confirmed` (or `submitted`): it left; `awaiting_approval`: it is waiting for signatures, funds reserved; `pending`: it is still executing, or its outcome is unknown and waits for an administrator's reconciliation; `failed`, `rejected` or `cancelled`: it did not leave. Tell the person that, not "sent".
|
|
487
|
+
- **A new key after a `failed` or `rejected` transfer.** The old key is bound to that row, and reusing it returns the dead transfer instead of sending a new one. The same key with any other parameters is not a replay at all: 409 `IDEMPOTENCY_KEY_REUSED`.
|
|
471
488
|
- **No key sent, and a timeout?** Do **not** retry. Fetch the transfer list or the operation history first and establish whether the transfer exists. Make the SDK's own callers always pass a key so this situation cannot arise.
|
|
472
489
|
- **`network` and `aborted` carry the same uncertainty as `timeout`** on money paths. The request may have been received and executed; only the response was lost. Treat all three identically: same key, or verify first.
|
|
473
490
|
- **A thrown `GAS_STATION_FAILED` or `WALLET_SERVICE_ERROR` is different.** Those are definite: the transfer is `failed` and the reserve has been returned. Retry those with a **new** key.
|
|
491
|
+
- **A thrown `TRANSFER_OUTCOME_UNKNOWN` is not.** It is a 502 that carries the same uncertainty as a timeout: the transaction may have left. Only the same key, which returns the transfer and does not execute it again; the outcome is set by reconciliation. `TRANSFER_BROADCAST_UNRECORDED` means it did leave — no retry at all.
|
|
474
492
|
- **Nothing a client does can leave funds locked.** Every failure path either returns the reserve or leaves a `pending` transfer that an admin can reconcile. Timeouts are a reporting problem, not a custody one — but a double send is permanent, so the key matters more than the retry.
|
|
475
493
|
|
|
476
494
|
## Declared but never returned
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@v3finance/core-sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "The official TypeScript SDK for the V3 Finance Core API — custodial addresses, transfers under policy control, approvals, shared access, labelling and reports",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// the API does not slip in unclassified.
|
|
10
10
|
//
|
|
11
11
|
// Operations: 171 · admin: 40 · both: 1 · member: 127 · service: 3
|
|
12
|
-
// Spec sha256:
|
|
12
|
+
// Spec sha256: 30be5f9b9733d1d1d67eebf21e0bfaa457651e951676211ec9fa464cca5d3795
|
|
13
13
|
|
|
14
14
|
export type Audience = 'member' | 'admin' | 'service' | 'both' | 'unknown'
|
|
15
15
|
|
package/src/generated/types.ts
CHANGED
|
@@ -322,6 +322,7 @@ export interface OutgoingTxSummary {
|
|
|
322
322
|
status: string
|
|
323
323
|
txHash: string | null
|
|
324
324
|
fee: string | null
|
|
325
|
+
/** Причина закрытия — только у failed/rejected/cancelled. У остальных статусов (awaiting_approval, pending, submitted, confirmed) — ВСЕГДА null: у незакрытого перевода причины отказа нет. Неясный исход исполнения (pending, транзакция могла уйти в сеть) здесь не показывается — его признак pending дольше 10 минут по updatedAt («ждёт сверки администратором») */
|
|
325
326
|
failureReason: string | null
|
|
326
327
|
/** format: date-time */
|
|
327
328
|
createdAt: string
|
|
@@ -1726,7 +1727,7 @@ export interface CreateTransactionResult {
|
|
|
1726
1727
|
amountDecimal: string
|
|
1727
1728
|
asset: string
|
|
1728
1729
|
network: string
|
|
1729
|
-
/**
|
|
1730
|
+
/** 201 — confirmed (исполнен сейчас); редко pending с заполненным txHash (транзакция ушла в сеть, а учёт не записался — доводит сверка, повторять перевод нельзя); 202 — awaiting_approval; 200 (replay по idempotencyKey) — ТЕКУЩИЙ статус перевода, любой: pending (первый запрос ещё исполняется — или его исход неизвестен: первый ответ был 502 TRANSFER_OUTCOME_UNKNOWN; повтор НЕ исполняет перевод заново, исход устанавливает сверка), awaiting_approval, confirmed, failed, rejected, cancelled */
|
|
1730
1731
|
status: string
|
|
1731
1732
|
txHash: string | null
|
|
1732
1733
|
fee: string | null
|
|
@@ -1741,12 +1742,13 @@ export interface CreateTransactionResult {
|
|
|
1741
1742
|
|
|
1742
1743
|
export interface CreateTransferBody {
|
|
1743
1744
|
fromAddressId: string
|
|
1745
|
+
/** Адрес получателя. Формат проверяется по типу сети адреса-отправителя ДО резерва средств: TRON — base58check (34 символа, «T…», контрольная сумма), EVM — 0x + 40 hex (смешанный регистр — контрольная сумма EIP-55). Не подошёл — 400 INVALID_DESTINATION_ADDRESS. Строка проверяется как есть, пробелы по краям — ошибка формата */
|
|
1744
1746
|
toAddress: string
|
|
1745
1747
|
/** DECIMAL строка — конвертируется по token.decimals */
|
|
1746
1748
|
amount: string
|
|
1747
1749
|
asset: string
|
|
1748
1750
|
/**
|
|
1749
|
-
* Уникальный ключ попытки перевода (рекомендуется: uuid). Повторный запрос с тем же ключом вернёт
|
|
1751
|
+
* Уникальный ключ попытки перевода (рекомендуется: uuid). Повторный запрос с тем же ключом и теми же параметрами вернёт 200 с существующим переводом — без второго broadcast, без повторной оценки политик. Тот же ключ с другими параметрами — 409 IDEMPOTENCY_KEY_REUSED. Для новой попытки после failed — новый ключ.
|
|
1750
1752
|
*
|
|
1751
1753
|
* length: 8..128
|
|
1752
1754
|
*/
|
|
@@ -2033,6 +2035,7 @@ export interface HistoryOutItem {
|
|
|
2033
2035
|
asset: string
|
|
2034
2036
|
assetSymbol: string
|
|
2035
2037
|
fee: string | null
|
|
2038
|
+
/** Причина закрытия — только у failed/rejected/cancelled. У остальных статусов (awaiting_approval, pending, submitted, confirmed) — ВСЕГДА null: у незакрытого перевода причины отказа нет. Неясный исход исполнения (pending, транзакция могла уйти в сеть) здесь не показывается — его признак pending дольше 10 минут по updatedAt («ждёт сверки администратором») */
|
|
2036
2039
|
failureReason: string | null
|
|
2037
2040
|
fromAddress: {
|
|
2038
2041
|
id: string
|
|
@@ -3173,7 +3176,7 @@ export interface RatesResponse {
|
|
|
3173
3176
|
}
|
|
3174
3177
|
|
|
3175
3178
|
export interface ReconcileTransferBody {
|
|
3176
|
-
/** Хеш от подписывающего сервиса. ЕСТЬ — транзакция ушла в сеть, перевод доводится до confirmed. НЕТ (или null) —
|
|
3179
|
+
/** Хеш от подписывающего сервиса. ЕСТЬ — транзакция ушла в сеть, перевод доводится до confirmed. НЕТ (или null) — сверяющий утверждает, что broadcast'а не было, и резерв возвращается (failed). Для pending с неясным исходом (у pending без txHash его не отличить по ответам API — failureReason у незакрытого перевода null) это утверждение нужно ПРОВЕРИТЬ по сети: возврат за ушедшую транзакцию печатает деньги. Если у pending-перевода хеш уже сохранён (broadcast прошёл), он обязателен: без него — 409 TRANSFER_BROADCAST, с другим — 409 TXHASH_MISMATCH (сохранённый — в error.details.txHash) */
|
|
3177
3180
|
txHash?: string | null
|
|
3178
3181
|
/** Комиссия, если известна */
|
|
3179
3182
|
fee?: string | null
|
|
@@ -3182,7 +3185,8 @@ export interface ReconcileTransferBody {
|
|
|
3182
3185
|
}
|
|
3183
3186
|
|
|
3184
3187
|
export interface ReconcileTransferResult {
|
|
3185
|
-
|
|
3188
|
+
/** Статус перевода после сверки: confirmed (по хешу) | failed (резерв возвращён). rejected/cancelled — у закрытого перевода с невозвращённым резервом: резерв возвращён, статус прежний */
|
|
3189
|
+
status: "confirmed" | "failed" | "rejected" | "cancelled"
|
|
3186
3190
|
txHash: string | null
|
|
3187
3191
|
}
|
|
3188
3192
|
|
|
@@ -3203,8 +3207,9 @@ export interface RequestVoteResult {
|
|
|
3203
3207
|
approvals: number
|
|
3204
3208
|
/** range: -9007199254740991..9007199254740991 */
|
|
3205
3209
|
threshold: number
|
|
3206
|
-
/** true — этот голос финализировал заявку (применена/отклонена) */
|
|
3210
|
+
/** true — этот голос финализировал заявку (применена/отклонена). У заявки на перевод status=APPROVED при finalized=false значит, что исход рассчитал НЕ этот вызов: applyError пуст — перевод исполняет (исполнил) параллельный запрос ЛИБО исполнение, начатое другим вызовом, оборвалось (сам перевод тогда не закроется); исход — только по GET /transfers/{id}: confirmed/failed, а pending дольше 10 минут по updatedAt — ждёт сверки (admin). applyError TRANSFER_EXECUTION_FAILED — к моменту одобрения перевод уже не ждал подписей. finalized=false с пустым applyError приходит и тогда, когда одобрил ЭТОТ голос, но исход перевода им не установлен (перевод pending: подписывающий сервис не дал определённого ответа, транзакция могла уйти в сеть, а могла и нет): показывайте «голос учтён», не «применено». finalized=true с пустым applyError у перевода — исполнен этим вызовом (confirmed) */
|
|
3207
3211
|
finalized: boolean
|
|
3212
|
+
/** Голос финализировал заявку, а применение не дало результата; status при этом итоговый. Коды: STALE — цель изменилась (status=REJECTED); APPLY_FAILED / APPLY_FAILED_ROLLED_BACK / APPLY_FAILED_PARTIAL — мутация не применена (целиком / цепочка откатана / частично). У action=transfer финальный approve ИСПОЛНЯЕТ перевод, status=APPROVED: TRANSFER_EXECUTION_FAILED — перевод не исполнен (обычно failed, резерв возвращён; точный статус — GET /transfers/{id}; при finalized=false — перевод уже не ждал подписей, одобрение его не исполняло). Неясный исход перевода (подписывающий сервис не дал определённого ответа, перевод pending, средства зарезервированы, транзакция могла уйти в сеть, исход установит сверка, admin) ошибкой НЕ отдаётся: applyError null, status APPROVED, finalized=false. Кода TRANSFER_OUTCOME_UNKNOWN здесь нет — он только у POST /transfers */
|
|
3208
3213
|
applyError: {
|
|
3209
3214
|
code: string
|
|
3210
3215
|
message: string
|
|
@@ -3376,6 +3381,7 @@ export interface TransactionDetail {
|
|
|
3376
3381
|
decisionPolicyCode: string | null
|
|
3377
3382
|
/** Человеческое название решающей политики (для UI вместо кода) */
|
|
3378
3383
|
decisionPolicyName: string | null
|
|
3384
|
+
/** Причина закрытия — только у failed/rejected/cancelled. У остальных статусов (awaiting_approval, pending, submitted, confirmed) — ВСЕГДА null: у незакрытого перевода причины отказа нет. Неясный исход исполнения (pending, транзакция могла уйти в сеть) здесь не показывается — его признак pending дольше 10 минут по updatedAt («ждёт сверки администратором») */
|
|
3379
3385
|
failureReason: string | null
|
|
3380
3386
|
/** format: date-time */
|
|
3381
3387
|
createdAt: string
|
|
@@ -3709,7 +3715,7 @@ export interface VoteRequestBody {
|
|
|
3709
3715
|
}
|
|
3710
3716
|
|
|
3711
3717
|
export interface VoteResult {
|
|
3712
|
-
/** Статус перевода ПОСЛЕ голоса: awaiting_approval (голосов мало) | rejected | confirmed/failed (финальный approve исполняет сразу) */
|
|
3718
|
+
/** Статус перевода ПОСЛЕ голоса: awaiting_approval (голосов мало) | rejected | confirmed/failed (финальный approve исполняет сразу). pending — исполнение не закончено, повторять перевод нельзя; executionError у pending всегда null, исход этим ответом НЕ установлен, ждать его «через секунды» нельзя. Так отдаётся и неясный исход (подписывающий сервис не дал определённого ответа, транзакция могла уйти в сеть, резерв стоит — доводит сверка, admin), и: txHash в GET /transfers/{id} есть — транзакция ушла в сеть, а учёт не записался, доводит сверка (admin) по этому хешу; перевод исполняет параллельный запрос (одновременный голос другого подписанта или исходный вызов при повторе голоса) ЛИБО исполнение оборвалось (клиент, который его начал, отключился — сам перевод тогда не закроется). Различает только время: перечитывайте GET; confirmed/failed — исход; pending дольше 10 минут по updatedAt — «ждёт сверки администратором» */
|
|
3713
3719
|
status: string
|
|
3714
3720
|
/**
|
|
3715
3721
|
* Собрано approve-голосов
|
|
@@ -3719,7 +3725,7 @@ export interface VoteResult {
|
|
|
3719
3725
|
approvals: number
|
|
3720
3726
|
/** range: -9007199254740991..9007199254740991 */
|
|
3721
3727
|
threshold: number
|
|
3722
|
-
/**
|
|
3728
|
+
/** Финальный approve запустил исполнение, и оно перевод НЕ исполнило. code: TRANSFER_EXECUTION_FAILED — не исполнен (status=failed, средства возвращены; исключения: failed с txHash у перевода, см. описание ручки, и status=pending, если сам возврат резерва не записался — тогда перевод доводит сверка). Тот же код — если к моменту одобрения перевод уже не ждал подписей (status — его фактический: failed/rejected/cancelled): одобрение его не исполнило. Неясный исход (подписывающий сервис не дал определённого ответа, транзакция могла уйти в сеть) ошибкой НЕ отдаётся: status=pending, executionError=null — показывайте «исполняется, исход уточняется». Кода TRANSFER_OUTCOME_UNKNOWN в голосе нет, он только у POST /transfers */
|
|
3723
3729
|
executionError: {
|
|
3724
3730
|
code: string
|
|
3725
3731
|
message: string
|
|
@@ -79,6 +79,28 @@ export class ApprovalsResource {
|
|
|
79
79
|
*
|
|
80
80
|
* Read `finalized` with `applyError`: a vote can finalize a request AND
|
|
81
81
|
* fail to apply it, and then the money has not moved.
|
|
82
|
+
*
|
|
83
|
+
* On a transfer request the approve that completes the threshold EXECUTES
|
|
84
|
+
* the transfer inside this call, and `status` is `APPROVED` whatever
|
|
85
|
+
* happened to the transfer:
|
|
86
|
+
*
|
|
87
|
+
* finalized, no applyError — executed by this call;
|
|
88
|
+
* applyError — `TRANSFER_EXECUTION_FAILED`, not executed
|
|
89
|
+
* (usually `failed`, reserve returned);
|
|
90
|
+
* not finalized, no applyError — the outcome is NOT established by this
|
|
91
|
+
* answer: the signing service gave no
|
|
92
|
+
* definite answer (the transaction may
|
|
93
|
+
* have left; an administrator's
|
|
94
|
+
* reconciliation settles it), another call
|
|
95
|
+
* is executing the transfer, or the
|
|
96
|
+
* execution broke off. Read the outcome
|
|
97
|
+
* from the transfer (`transfers.get`), and
|
|
98
|
+
* never resubmit it.
|
|
99
|
+
*
|
|
100
|
+
* Repeating the same decision on a request that is still open finishes what
|
|
101
|
+
* an earlier call left undone (it recorded the vote and broke off) — a veto,
|
|
102
|
+
* or an approval once the threshold is met — instead of 409
|
|
103
|
+
* `ALREADY_VOTED`.
|
|
82
104
|
*/
|
|
83
105
|
vote(requestId: string, input: VoteBody): Promise<RequestVoteResult> {
|
|
84
106
|
return this.#http.request<RequestVoteResult>(
|