@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +2 -1
  3. package/dist/cjs/generated/operations.js +1 -1
  4. package/dist/cjs/generated/types.d.ts +13 -7
  5. package/dist/cjs/generated/types.d.ts.map +1 -1
  6. package/dist/cjs/resources/approvals.d.ts +22 -0
  7. package/dist/cjs/resources/approvals.d.ts.map +1 -1
  8. package/dist/cjs/resources/approvals.js +22 -0
  9. package/dist/cjs/resources/approvals.js.map +1 -1
  10. package/dist/cjs/resources/transfers.d.ts +64 -16
  11. package/dist/cjs/resources/transfers.d.ts.map +1 -1
  12. package/dist/cjs/resources/transfers.js +64 -16
  13. package/dist/cjs/resources/transfers.js.map +1 -1
  14. package/dist/cjs/version.d.ts +1 -1
  15. package/dist/cjs/version.js +1 -1
  16. package/dist/esm/generated/operations.js +1 -1
  17. package/dist/esm/generated/types.d.ts +13 -7
  18. package/dist/esm/generated/types.d.ts.map +1 -1
  19. package/dist/esm/resources/approvals.d.ts +22 -0
  20. package/dist/esm/resources/approvals.d.ts.map +1 -1
  21. package/dist/esm/resources/approvals.js +22 -0
  22. package/dist/esm/resources/approvals.js.map +1 -1
  23. package/dist/esm/resources/transfers.d.ts +64 -16
  24. package/dist/esm/resources/transfers.d.ts.map +1 -1
  25. package/dist/esm/resources/transfers.js +64 -16
  26. package/dist/esm/resources/transfers.js.map +1 -1
  27. package/dist/esm/version.d.ts +1 -1
  28. package/dist/esm/version.js +1 -1
  29. package/docs/concepts.md +2 -1
  30. package/docs/errors.md +36 -16
  31. package/package.json +1 -1
  32. package/src/generated/operations.ts +1 -1
  33. package/src/generated/types.ts +13 -7
  34. package/src/resources/approvals.ts +22 -0
  35. package/src/resources/transfers.ts +64 -16
  36. 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 stuck in `pending` — the case where the worker died
88
- * mid-execution, so cancelling does not apply. Administrators only.
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
- * The decision is made by the HASH: send `txHash` and the transfer is
91
- * driven to `confirmed`, because the signing service did put it on the
92
- * chain; send none and the reserve is returned, because there was no
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
- * it can be recorded on a transfer settled this way, and it stays null
99
- * otherwise. The answer says which way it went — `confirmed` or `failed`.
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;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,iBAAiB;;IAI5B,YAAY,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAG9C;IAED;;;;;;OAMG;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;;;;OAIG;IACH,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAElD;IAED;;;;;;;;;;;;;;;;;;OAkBG;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;;;;;;;;;;;;;;OAcG;IACH,SAAS,CACP,UAAU,EAAE,MAAM,EAClB,KAAK,GAAE,qBAA0B,GAChC,OAAO,CAAC,uBAAuB,CAAC,CAMlC;CACF"}
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 stuck in `pending` — the case where the worker died
100
- * mid-execution, so cancelling does not apply. Administrators only.
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
- * The decision is made by the HASH: send `txHash` and the transfer is
103
- * driven to `confirmed`, because the signing service did put it on the
104
- * chain; send none and the reserve is returned, because there was no
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
- * it can be recorded on a transfer settled this way, and it stays null
111
- * otherwise. The answer says which way it went — `confirmed` or `failed`.
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;;;;;;;;;;;;;;;;;;;;;;GAsBG;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;;;;;;OAMG;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;;;;OAIG;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;;;;;;;;;;;;;;;;;;OAkBG;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;;;;;;;;;;;;;;OAcG;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"}
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"}
@@ -1,5 +1,5 @@
1
1
  /** SDK version, sent in the `user-agent` header. */
2
- export declare const SDK_VERSION = "0.2.1";
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
@@ -1,5 +1,5 @@
1
1
  /** SDK version, sent in the `user-agent` header. */
2
- export const SDK_VERSION = '0.2.1';
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`, `cancelled`).
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` | 502, or 400 when the signing service itself answered 4xx | The signing service was unreachable, errored, or returned no transaction id. The transfer is `failed` and the reserve is returned. | Same as above: retry with a new key. On the 400 variant the signing service rejected the request and a retry will fail identically — but `message` now gives only its status (`Wallet Service returned 400`): the service's own body is not echoed to clients, it goes to the worker log. Show that the signing service refused and report the transfer id; do not try to explain the refusal from `message`. |
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 left in `pending` because the worker died mid-execution — cancel does not reach those, it requires `awaiting_approval`.
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 entered `pending` less than 10 minutes ago. | **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). |
137
- | `TRANSFER_NOT_STUCK` | 409 | The transfer is not in `pending` at all, or was closed by another path while you were reconciling. | Re-read it. It resolved itself; nothing to do. |
138
- | `RESERVE_ALREADY_CONSUMED` | 409 | You asked to return the reserve, but a `confirm` posting has already spent it — the transaction went to the network. | **Never retry as a release.** The money left. Finish the transfer by supplying its `txHash` instead. |
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 then failed — the reserve has been returned.
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 | You already have a folder with that name (names are unique per owner, not per vault). | Ask for a different name. |
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`; which failures reach it is not evident from the code, so "retry once with a new key, then escalate" is the honest guidance rather than a specific cause.
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.1",
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: 017bae30075b232ab4efdcfb7785335020be3060b46e3569b1cd887fcf037597
12
+ // Spec sha256: 1f0f2a38668572c443a84cb0207d4070b419a16c293b8e975f5e21c68e85b15a
13
13
 
14
14
  export type Audience = 'member' | 'admin' | 'service' | 'both' | 'unknown'
15
15
 
@@ -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
- /** confirmed — новый успешный перевод; pending/failed возможны при replay по idempotencyKey */
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). Повторный запрос с тем же ключом вернёт существующую транзакцию без второго broadcast. Для новой попытки после failed — новый ключ.
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) — считаем, что broadcast'а не было, и возвращаем резерв (failed) */
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
- status: "confirmed" | "failed"
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
- /** Если финальный approve запустил исполнение и оно упало (средства возвращены) */
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