@percayso-community/cayso-contracts 0.26.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -319,6 +319,28 @@
319
319
  * was served, as `'provision-citizenship'` was, and it is served at
320
320
  * `ROUTES.commercialPayment`.
321
321
  *
322
+ * `'make-citizen-transfer'` is a Citizen transferring Cayso from their own Citizen
323
+ * Wallet to another Citizen's (CAY-0088). It is not a second use of
324
+ * `'make-commercial-payment'`, which always has an organisation on one side, nor of
325
+ * `'place-quote'`, which prices cents against sterling: a transfer quotes nothing. A
326
+ * credential minted to place a quote must not thereby move cents to somebody without
327
+ * a match, nor the reverse. It is served at `ROUTES.citizenTransfer`.
328
+ *
329
+ * `'change-handle'` is a Citizen's Handle change, charged by Cayso (CAY-0091). It is
330
+ * not a second use of `'make-citizen-transfer'`, which pays another Citizen, nor of
331
+ * `'settle-marketplace-order'`, which presents Store Platform's signed order: a Handle
332
+ * change charges the Citizen for a Community service on the authority of a change
333
+ * reference Percayso ID minted. A credential minted for any other operation must not
334
+ * authorise a Handle change, and the reverse. It is served at `ROUTES.handleChange`,
335
+ * and it charges for the change reference named by
336
+ * {@link AssertionClaims.handleChangeReference} and no other.
337
+ *
338
+ * `'read-handle-change-receipt'` asks whether a change reference was charged
339
+ * (CAY-0091), as `'read-marketplace-receipt'` asks of a checkout key. It is its own
340
+ * operation because it is a read beside the money-grade charge it reports on: a
341
+ * credential minted to ask what happened must not thereby be able to make something
342
+ * happen. It is served at `ROUTES.handleChangeReceipt`.
343
+ *
322
344
  * `'place-quote'`, `'withdraw-quote'` and `'request-buy-back'` are the citizen
323
345
  * market's three acts (CAY-0080, Open decision Q). They are three operations
324
346
  * because they are three acts: a credential minted to withdraw a quote must not
@@ -363,7 +385,7 @@
363
385
  * Contribution Store's assertion, which no person presents and which therefore has
364
386
  * no operation here. It is classified before it is served.
365
387
  */
366
- export type ContractOperation = 'read-own-wallet' | 'read-purchase-result' | 'authorise-purchase' | 'cancel-purchase' | 'provision-citizenship' | 'read-own-citizenship' | 'provision-organisation' | 'claim-founder-wallet' | 'make-commercial-payment' | 'place-quote' | 'withdraw-quote' | 'request-buy-back' | 'read-citizen-market' | 'read-marketplace-receipt' | 'settle-marketplace-order' | 'settle-marketplace-refund' | 'present-standing-authorisation';
388
+ export type ContractOperation = 'read-own-wallet' | 'read-purchase-result' | 'authorise-purchase' | 'cancel-purchase' | 'provision-citizenship' | 'read-own-citizenship' | 'provision-organisation' | 'claim-founder-wallet' | 'make-commercial-payment' | 'make-citizen-transfer' | 'change-handle' | 'read-handle-change-receipt' | 'place-quote' | 'withdraw-quote' | 'request-buy-back' | 'read-citizen-market' | 'read-marketplace-receipt' | 'settle-marketplace-order' | 'settle-marketplace-refund' | 'present-standing-authorisation';
367
389
  /**
368
390
  * An operation key belonging to a deployment's OWN surfaces rather than to this
369
391
  * contract — operator work, above all. Deliberately not enumerated: this package
@@ -832,6 +854,42 @@ export interface AssertionClaims {
832
854
  * of work.
833
855
  */
834
856
  readonly founderClaimReference?: string;
857
+ /**
858
+ * The change reference: the single-use, opaque value Percayso ID issued when it
859
+ * reserved the Citizen's proposed Handle (CAY-0091). OPTIONAL on the wire, and read by
860
+ * `'change-handle'` and `'read-handle-change-receipt'` and by no other operation.
861
+ *
862
+ * ## It travels with its operations, in both directions
863
+ *
864
+ * As {@link founderClaimReference} does. A credential whose `scope` carries
865
+ * `'change-handle'` or `'read-handle-change-receipt'` must carry this claim, and a
866
+ * credential that carries this claim must be scoped to one of them. A verifier refuses
867
+ * either half missing as a malformed credential, before any gate reads it: a Handle
868
+ * change with no reference charges for nothing, and a reference carried to any other
869
+ * operation is half of a Handle change put in front of a route with no business
870
+ * reading it.
871
+ *
872
+ * ## It is carried here and never in a request
873
+ *
874
+ * The reference fixes the new Handle BEFORE the charge, so a receipt cannot be spent
875
+ * on a different name. A reference named in a body would be the caller's claim about
876
+ * which Handle change is theirs (CAY-0021); inside the signed assertion it is Percayso
877
+ * ID's statement. Cayso charges at most once per reference, keys the charge by it, and
878
+ * names it in the signed receipt.
879
+ *
880
+ * ## What it is not
881
+ *
882
+ * It is NOT the Handle. Cayso never learns the Handle: the reference is opaque, and
883
+ * Cayso compares it and never parses it. It is not derived from a Percayso identity,
884
+ * and it carries no `sub`, no name and no email. This contract asks for a non-empty
885
+ * string and nothing more.
886
+ *
887
+ * ## It is as trustworthy as the verifier, and no more
888
+ *
889
+ * As with every claim: declaring it makes the promise structural, and a real
890
+ * signature check is what makes it trustworthy.
891
+ */
892
+ readonly handleChangeReference?: string;
835
893
  /**
836
894
  * The Organisation **Wallet IDs** this identity holds the mandate for
837
895
  * (CAY-0024, CAY-0059 §7). Each entry is a wallet id — the opaque identifier
package/dist/assurance.js CHANGED
@@ -325,6 +325,51 @@ export const ASSURANCE_POLICY = {
325
325
  'money-grade: anything but the development bypass. The bar applies on both routes, and to ' +
326
326
  'an Authorised User it applies again at every payment, not only when the allowance was granted.',
327
327
  },
328
+ // A Citizen transferring to another Citizen (CAY-0088 §2). Money-grade, at the
329
+ // purchase path's bar, for the commercial payment's reason: value leaves a holder
330
+ // for somebody else, and a bar no issuer can clear would close the transfer on
331
+ // every deployment. An allowlist, so a future method is admissible only when
332
+ // somebody decides it here.
333
+ 'make-citizen-transfer': {
334
+ operation: 'make-citizen-transfer',
335
+ assurance: 'required',
336
+ decisions: {
337
+ // Asserts nothing; permanently refused wherever "identity-verified" is meant.
338
+ 'dev-bypass': 'refused',
339
+ document: 'accepted',
340
+ 'document-and-biometric': 'accepted',
341
+ },
342
+ rationale: 'A Citizen transfer moves value out of one holder to another, so it is money-grade: ' +
343
+ 'anything but the development bypass. An unverified holder must never be able to send, ' +
344
+ 'because sending is how value reaches somebody else.',
345
+ },
346
+ // A Citizen paying Cayso to change their Handle (CAY-0091). Money-grade, at the
347
+ // Citizen transfer's bar: value leaves the holder, and the payment is what lets
348
+ // Percayso ID change a public name, so an unverified holder must not be able to
349
+ // make it. An allowlist, so a future method is admissible only when somebody
350
+ // decides it here.
351
+ 'change-handle': {
352
+ operation: 'change-handle',
353
+ assurance: 'required',
354
+ decisions: {
355
+ // Asserts nothing; permanently refused wherever "identity-verified" is meant.
356
+ 'dev-bypass': 'refused',
357
+ document: 'accepted',
358
+ 'document-and-biometric': 'accepted',
359
+ },
360
+ rationale: "A Handle change charges the Citizen's own Citizen Wallet, so it is money-grade: anything " +
361
+ 'but the development bypass. The receipt it answers with lets Percayso ID change a ' +
362
+ 'Handle, which an unverified holder must never be able to buy.',
363
+ },
364
+ // Asking whether a change reference was charged (CAY-0091). A read: nothing moves and
365
+ // nothing is reserved, so it applies no bar, as the marketplace receipt read applies
366
+ // none. A credential scoped to it cannot charge anything.
367
+ 'read-handle-change-receipt': {
368
+ operation: 'read-handle-change-receipt',
369
+ assurance: 'not-required',
370
+ rationale: 'Asking whether a change reference was charged moves nothing and reserves nothing. It ' +
371
+ 'answers a receipt Cayso already signed, or that there was no charge, and nothing more.',
372
+ },
328
373
  // The citizen market's three acts (CAY-0080 §7, Open decision Q). All three are
329
374
  // money-grade and all three are allowlists, at the purchase path's bar: the acting
330
375
  // party is a Citizen, and a bar no issuer can yet clear would shut the market on
@@ -505,6 +550,9 @@ export const OPERATION_FOR_ROUTE = {
505
550
  purchaseResult: 'read-purchase-result',
506
551
  purchaseCancel: 'cancel-purchase',
507
552
  commercialPayment: 'make-commercial-payment',
553
+ citizenTransfer: 'make-citizen-transfer',
554
+ handleChange: 'change-handle',
555
+ handleChangeReceipt: 'read-handle-change-receipt',
508
556
  citizenMarket: 'read-citizen-market',
509
557
  placeQuote: 'place-quote',
510
558
  withdrawQuote: 'withdraw-quote',
@@ -0,0 +1,110 @@
1
+ /**
2
+ * `citizen-transfer` — the request, the answer and the closed refusal union for a
3
+ * Citizen transferring Cayso to another Citizen (CAY-0088).
4
+ *
5
+ * A Citizen transfer is unrestricted cents moving directly from the caller's own
6
+ * Citizen Wallet to another Citizen Wallet, as one `transfer` movement. It is not a
7
+ * quoted sale and it is not the citizen market: no price is quoted, no sterling moves,
8
+ * and the Treasury takes no part. A transfer between a pair that mirrors market fills
9
+ * is watched for by the transfer-pattern alarm (CAY-0080 §5.8) and is reported, never
10
+ * reversed. Served at `ROUTES.citizenTransfer`.
11
+ *
12
+ * ## One movement or nothing, so there is no status
13
+ *
14
+ * As with the commercial payment (CAY-0079 §5) there is no sterling leg and no
15
+ * provider, so there is nothing to authorise first and capture later. The answer is a
16
+ * {@link CitizenTransferMade}, carrying the one movement that was written, or a
17
+ * {@link CitizenTransferRefused}, carrying one reason and meaning nothing was written.
18
+ *
19
+ * ## Replay under one key
20
+ *
21
+ * The key is unique together with the sending wallet. A repeat with the same key, the
22
+ * same payee and the same cents answers the movement already written, in the same
23
+ * shape; a repeat with the same key and anything different is refused with
24
+ * `idempotency_key_reused_for_a_different_transfer`. A transfer is never reversed: a
25
+ * Citizen who wants cents back asks for a new transfer the other way, made by the
26
+ * other Citizen under their own key.
27
+ *
28
+ * ## The sender is never named in the request
29
+ *
30
+ * The sending wallet is the verified assertion's subject's own Citizen Wallet,
31
+ * resolved inside the service (CAY-0021). A request that named the sending wallet
32
+ * would be the caller's claim about whose cents these are. The payee is a wallet id,
33
+ * given by the payee as they would give an address, and never a Percayso identity.
34
+ *
35
+ * ## What this file must not decide
36
+ *
37
+ * - **No figures.** No ceiling, rate or threshold is restated here (CAY-0088 §4).
38
+ * - **One payee.** A split is not designed.
39
+ * - **Restricted cents never move.** A restricted lot is not transferable (CAY-0074),
40
+ * so a wallet short of unrestricted cents is short, whatever else it holds.
41
+ */
42
+ import type { Cents, Millis } from './wallet.js';
43
+ /** A Citizen transferring cents from their own Citizen Wallet to another, under one key. */
44
+ export interface CitizenTransferRequest {
45
+ /** The Citizen Wallet the cents arrive in. A wallet id, never a Percayso identity. */
46
+ readonly payeeWalletId: string;
47
+ /** Whole cents, above zero. Anything else is refused `amount_not_positive_whole_cents`. */
48
+ readonly cents: Cents;
49
+ /** Caller-generated, unique per sending wallet. The same key never moves cents twice; a repeat REPLAYS. */
50
+ readonly idempotencyKey: string;
51
+ }
52
+ /**
53
+ * The one movement a Citizen transfer wrote. Final and append-only. It names the
54
+ * sending wallet because the caller is owed the handle their own cents left from, and
55
+ * names no person.
56
+ */
57
+ export interface CitizenTransfer {
58
+ /** The movement that carried the cents. */
59
+ readonly movementId: string;
60
+ readonly fromWalletId: string;
61
+ readonly payeeWalletId: string;
62
+ readonly cents: Cents;
63
+ readonly transferredAt: Millis;
64
+ }
65
+ /** The transfer was made. Answered for the request that made it and for every replay of its key. */
66
+ export interface CitizenTransferMade {
67
+ readonly outcome: 'transferred';
68
+ readonly transfer: CitizenTransfer;
69
+ }
70
+ /** The transfer was refused, with one member of the closed union. Nothing was written. */
71
+ export interface CitizenTransferRefused {
72
+ readonly outcome: 'refused';
73
+ readonly error: CitizenTransferRefusal;
74
+ }
75
+ /** The ONE declared answer to a Citizen transfer, discriminated by `outcome`. */
76
+ export type CitizenTransferResponse = CitizenTransferMade | CitizenTransferRefused;
77
+ /**
78
+ * Why a Citizen transfer was refused: CAY-0088 §3's set, closed. A consumer renders a
79
+ * member by its name. Every member means the same thing about money: nothing moved.
80
+ */
81
+ export type CitizenTransferRefusal =
82
+ /**
83
+ * The verification method is outside the money-grade assurance profile
84
+ * (`ASSURANCE_POLICY['make-citizen-transfer']`). `dev-bypass` is never inside it.
85
+ */
86
+ 'assurance_not_money_grade'
87
+ /** The verified subject resolves to no Citizen Wallet. A transfer never provisions one. */
88
+ | 'sender_has_no_citizen_wallet'
89
+ /** No wallet has the payee's id. */
90
+ | 'payee_wallet_not_found'
91
+ /** The payee's wallet is an Organisation Wallet or the Treasury. Organisations are paid by commercial payment. */
92
+ | 'payee_wallet_not_a_citizens'
93
+ /** No citizen is bound to the payee's wallet now, for example because the binding was erased. */
94
+ | 'payee_wallet_has_no_live_binding'
95
+ /** The payee's wallet is the sender's own. */
96
+ | 'payee_is_the_sender'
97
+ /** The cents are zero, negative or not a whole number. */
98
+ | 'amount_not_positive_whole_cents'
99
+ /**
100
+ * The sender's unrestricted cents are short. Restricted cents are never counted and
101
+ * never drawn (CAY-0074), so a wallet holding enough only once restricted cents are
102
+ * included is still short.
103
+ */
104
+ | 'unrestricted_cents_insufficient'
105
+ /** A holding the transfer would draw is frozen. */
106
+ | 'cents_frozen'
107
+ /** The key was already used by this sending wallet for a different payee or a different number of cents. */
108
+ | 'idempotency_key_reused_for_a_different_transfer'
109
+ /** Too many concurrent draws on the sending wallet. Nothing moved; retry under the same key. */
110
+ | 'carve_contended';
@@ -0,0 +1,16 @@
1
+ /*
2
+ * Copyright 2026 Percayso Community
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ export {};
package/dist/client.d.ts CHANGED
@@ -70,7 +70,9 @@
70
70
  import type { BuyBackMade, BuyBackRequest, CitizenMarketViewed, QuotePlaced, QuoteRequest, QuoteWithdrawalRequest, QuoteWithdrawn } from './citizen-market.js';
71
71
  import type { CitizenshipFounderClaimed, CitizenshipView } from './citizenship.js';
72
72
  import type { StandingAuthorisationAcknowledged, StandingAuthorisationRequest } from './cloud-settlement.js';
73
+ import type { CitizenTransferMade, CitizenTransferRequest } from './citizen-transfer.js';
73
74
  import type { CommercialPaymentMade, CommercialPaymentRequest } from './commercial-payment.js';
75
+ import type { HandleChangeCharged, HandleChangeReceiptReadResponse } from './handle-change.js';
74
76
  import type { MarketplaceOrderRequest, MarketplaceOrderSettled } from './marketplace-settlement.js';
75
77
  import type { OrganisationView } from './organisation.js';
76
78
  import type { PurchaseAuthorised, PurchaseCancelled, PurchaseRequest, PurchaseResult, PurchaseTerminal } from './purchase.js';
@@ -213,6 +215,35 @@ export interface CaysoClient {
213
215
  * refusals do. A refusal means nothing moved.
214
216
  */
215
217
  makeCommercialPayment(req: CommercialPaymentRequest): Promise<CommercialPaymentMade>;
218
+ /**
219
+ * Transfer unrestricted cents from the caller's own Citizen Wallet to another
220
+ * Citizen Wallet — or REPLAY a transfer already made under the same key (CAY-0088).
221
+ *
222
+ * The sending wallet is never named: it is the verified holder's own. A replay with
223
+ * the same key, payee and cents answers the movement already written. A refusal
224
+ * arrives as a thrown {@link CaysoApiError} whose `code` is a
225
+ * `CitizenTransferRefusal` member, one of the gate's codes, or a seam code. A
226
+ * refusal means nothing moved.
227
+ */
228
+ makeCitizenTransfer(req: CitizenTransferRequest): Promise<CitizenTransferMade>;
229
+ /**
230
+ * Charge the caller's own Citizen Wallet for a Handle change, and receive the signed
231
+ * receipt Percayso ID relies on (CAY-0091) — or, for a change reference already
232
+ * charged, the receipt of that charge, with nothing charged again.
233
+ *
234
+ * There is nothing to send: the change reference is inside the assertion, the wallet
235
+ * is the verified holder's own, and the price is Cayso's. A refusal arrives as a thrown
236
+ * {@link CaysoApiError} whose `code` is a `HandleChangeRefusal` member, one of the
237
+ * gate's codes, or a seam code. A refusal means nothing moved.
238
+ */
239
+ changeHandle(): Promise<HandleChangeCharged>;
240
+ /**
241
+ * Ask whether the change reference inside the assertion was charged for the caller
242
+ * (CAY-0091): its receipt, or `no-charge`. Never "pending". A refusal arrives as a
243
+ * thrown {@link CaysoApiError} whose `code` is a `HandleChangeReceiptReadRefusal`
244
+ * member, one of the gate's codes, or a seam code — and is never "no charge".
245
+ */
246
+ readHandleChangeReceipt(): Promise<HandleChangeReceiptReadResponse>;
216
247
  /**
217
248
  * Read the citizen market as the caller sees it (CAY-0080 §1.4): the published
218
249
  * value and its band, both bounds per quote unit, the best bid and the best ask with
package/dist/client.js CHANGED
@@ -221,6 +221,35 @@ function buildClient(options, internals) {
221
221
  idempotencyKey: req.idempotencyKey,
222
222
  });
223
223
  },
224
+ async makeCitizenTransfer(req) {
225
+ return requestJson({
226
+ method: ROUTES.citizenTransfer.method,
227
+ path: ROUTES.citizenTransfer.path,
228
+ // Safe to repeat only because of the key: a repeat replays the transfer.
229
+ idempotent: typeof req.idempotencyKey === 'string' && req.idempotencyKey.length > 0,
230
+ body: req,
231
+ idempotencyKey: req.idempotencyKey,
232
+ });
233
+ },
234
+ async changeHandle() {
235
+ return requestJson({
236
+ method: ROUTES.handleChange.method,
237
+ path: ROUTES.handleChange.path,
238
+ // Safe to retry with no key: the charge is keyed by the change reference
239
+ // inside the assertion, so a repeat answers the charge already made, and each
240
+ // attempt mints a fresh assertion, so the single-use `jti` is never presented
241
+ // twice.
242
+ idempotent: true,
243
+ });
244
+ },
245
+ async readHandleChangeReceipt() {
246
+ return requestJson({
247
+ method: ROUTES.handleChangeReceipt.method,
248
+ path: ROUTES.handleChangeReceipt.path,
249
+ // A read: it moves nothing, so a retry is always safe.
250
+ idempotent: true,
251
+ });
252
+ },
224
253
  async getCitizenMarket() {
225
254
  return requestJson({
226
255
  method: ROUTES.citizenMarket.method,