@v3finance/core-sdk 0.2.1 → 0.2.3
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 +49 -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 +36 -16
- 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.3";
|
|
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.3';
|
|
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:
|
|
@@ -178,8 +194,10 @@ Two sets of uppercase strings travel inside **successful** bodies and will misle
|
|
|
178
194
|
| `NO_DYNAMIC_ROUTE` | 500 | The vault's derivation scheme has no incrementing segment. | Configuration fault — address creation is impossible in this vault until an admin fixes it. |
|
|
179
195
|
| `TOKEN_NOT_REGISTERED` | 500 | An asset segment points at a token missing from the registry. | Configuration fault. Contact an administrator. |
|
|
180
196
|
| `POLICY_DENIED` | 403 | `block`, or no policy permits `create_address`. `details` holds the decision. | Terminal. Show `message`. |
|
|
197
|
+
| `APPROVAL_UNSATISFIABLE` | 409 | `create_address` requires signatures, but fewer people can give them than the threshold asks for (the initiator does not count unless the policy allows approving one's own request). Neither the address nor an approval request was created. Unlike the transfer's, it carries no `details`. | **Do not retry.** An administrator has to change the policy or the approver group. See [Easily confused](#easily-confused). |
|
|
198
|
+
| `APPROVAL_FAILED` | 409 | The approval request could not be created for a reason other than `APPROVAL_UNSATISFIABLE`. See the note under [Declared but never returned](#declared-but-never-returned). | Retry once. If it repeats, it is a server-side fault — escalate. |
|
|
181
199
|
|
|
182
|
-
Address creation that requires signatures is **not** an error: the server answers **202** with `{status:"awaiting_approval", approvalRequestId, approvalThreshold}`. Nothing is thrown. (`POLICY_REQUIRES_APPROVAL` exists in the server's type but is never sent — see [Declared but never returned](#declared-but-never-returned).)
|
|
200
|
+
Address creation that requires signatures is **not** an error: the server answers **202** with `{status:"awaiting_approval", approvalRequestId, approvalThreshold}`. Nothing is thrown. (`POLICY_REQUIRES_APPROVAL` exists in the server's type but is never sent — see [Declared but never returned](#declared-but-never-returned).) The exception is signatures that cannot be gathered at all: then there is no request and no 202, but `APPROVAL_UNSATISFIABLE` above.
|
|
183
201
|
|
|
184
202
|
Creating an address **into a folder** (`groupId`) can succeed without the folder, and that is not an error either. The right to the folder is checked before anything is created — a refusal there is `GROUP_CREATE_FORBIDDEN` above — but the address is put into the folder only after the signing service and monitoring, and the right is checked again at that moment. If that fails, the address still exists and the answer is still **201**, with `warnings: [{code, message}]` in the body:
|
|
185
203
|
|
|
@@ -209,7 +227,7 @@ Address shares, folder shares, member-visibility grants. All grant `POST`s are i
|
|
|
209
227
|
| `NOT_FOLDER_MANAGER` | 403 | Only the folder's creator changes its contents. | Terminal. Ask the creator. |
|
|
210
228
|
| `NOT_ADDRESS_OWNER` | 403 | You manage the folder, but the address you are adding is not yours — putting it in a shared folder would widen access to it behind its owner's back. | Terminal, and deliberately so. Only the address's own owner can add it. |
|
|
211
229
|
| `ALREADY_MEMBER` | 409 | The address is already in the folder. | Benign. Treat as success and refresh. |
|
|
212
|
-
| `GROUP_NAME_CONFLICT` | 409 |
|
|
230
|
+
| `GROUP_NAME_CONFLICT` | 409 | Creating a folder, or renaming one (`PATCH`), with a name its owner already uses for another folder (names are unique per owner, not per vault). | Ask for a different name. |
|
|
213
231
|
| `ACCESS_VIA_GROUP` | 409 | You tried to remove your own access, but it comes from a user group, not a personal grant. `details` has `{groupId, groupName}`. | Explain it with the group name from `details`: the user must leave the group, or an admin must revoke the group's grant. |
|
|
214
232
|
| `NOT_YOURS` | 403 | You tried to remove someone else's grant via the "remove me" endpoint. | A client bug. Revoking another person's access is the object owner's prerogative. |
|
|
215
233
|
| `FORBIDDEN` | 403 | Generic refusal when removing an address from a folder. | Terminal. |
|
|
@@ -268,7 +286,8 @@ In the single-subject form the per-subject code is returned as a normal error
|
|
|
268
286
|
| `NOT_FOUND` | 404 | No such request in this vault. | Refresh the list. |
|
|
269
287
|
| `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
288
|
| `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. |
|
|
289
|
+
| `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. |
|
|
290
|
+
| `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
291
|
| `NOT_INITIATOR` | 403 | Only the initiator or an admin can cancel. | Hide the cancel control. |
|
|
273
292
|
| `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
293
|
| `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. |
|
|
@@ -409,11 +428,11 @@ Rate limiting outside `/api/auth/*` is not implemented in the API itself. A 429
|
|
|
409
428
|
|
|
410
429
|
## Easily confused
|
|
411
430
|
|
|
412
|
-
**`APPROVAL_UNSATISFIABLE` / `UNSATISFIABLE` is not a transient conflict.** It is a 409, and 409s are usually worth a retry. This one never is: it means the policy demands more signatures than there are people able to give them, so every retry fails identically. The only resolution is an administrator changing the policy or the approver group. Say that, and do not offer a retry button. On a transfer, the reserve has already been returned when you see this, so no funds are stuck.
|
|
431
|
+
**`APPROVAL_UNSATISFIABLE` / `UNSATISFIABLE` is not a transient conflict.** It is a 409, and 409s are usually worth a retry. This one never is: it means the policy demands more signatures than there are people able to give them, so every retry fails identically. The only resolution is an administrator changing the policy or the approver group. Say that, and do not offer a retry button. On a transfer, the reserve has already been returned when you see this, so no funds are stuck; on address creation, neither the address nor a request exists.
|
|
413
432
|
|
|
414
433
|
**`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
434
|
|
|
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.
|
|
435
|
+
**`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
436
|
|
|
418
437
|
**`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
438
|
|
|
@@ -466,11 +485,12 @@ else showByStatus(result.data.transaction.status) // replayed: NOT necessarily s
|
|
|
466
485
|
Rules that follow from how the server handles the key:
|
|
467
486
|
|
|
468
487
|
- **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.
|
|
488
|
+
- **`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".
|
|
489
|
+
- **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
490
|
- **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
491
|
- **`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
492
|
- **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.
|
|
493
|
+
- **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
494
|
- **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
495
|
|
|
476
496
|
## Declared but never returned
|
|
@@ -488,4 +508,4 @@ These codes exist on the server and never reach a client. Most are simply never
|
|
|
488
508
|
|
|
489
509
|
Two further codes are reachable only through server misconfiguration and are worth knowing so you classify them correctly rather than showing them to a user: `DB_REQUIRED` (500, a money path reached without its database binding — fail-closed, nothing is left locked) and `VAULT_HAS_NO_WALLET_SERVICE` / `NO_DYNAMIC_ROUTE` / `TOKEN_NOT_REGISTERED` (500, vault configuration gaps that make transfers or address creation impossible until an administrator fixes them).
|
|
490
510
|
|
|
491
|
-
**Conditions I could not pin down:** `TRANSFER_NOT_EXECUTABLE` is thrown from several places with three different statuses (404 when the transfer is missing, 409 when it is not `pending`, 500 when its execution context fails to load), so apart from "re-read the transfer and trust its status" there is no single client action that fits all of them. `APPROVAL_FAILED` is the fallback for any approval-request creation failure that is not `UNSATISFIABLE
|
|
511
|
+
**Conditions I could not pin down:** `TRANSFER_NOT_EXECUTABLE` is thrown from several places with three different statuses (404 when the transfer is missing, 409 when it is not `pending`, 500 when its execution context fails to load), so apart from "re-read the transfer and trust its status" there is no single client action that fits all of them. `APPROVAL_FAILED` is the fallback for any approval-request creation failure that is not `UNSATISFIABLE`, on a transfer and on address creation; which failures reach it is not evident from the code, so "retry once (on a transfer, with a new key), then escalate" is the honest guidance rather than a specific cause.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@v3finance/core-sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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: 1f0f2a38668572c443a84cb0207d4070b419a16c293b8e975f5e21c68e85b15a
|
|
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
|