@t-0/provider-starter-ts 1.1.13 → 1.1.15

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/README.md CHANGED
@@ -15,19 +15,25 @@ The CLI will prompt for a project name, then create a ready-to-run project with
15
15
  ```
16
16
  your-project-name/
17
17
  ├── src/
18
- │ ├── index.ts # Entry point
19
- │ ├── service.ts # Provider service implementation
20
- │ ├── publish_quotes.ts # Quote publishing logic
21
- │ ├── get_quote.ts # Quote retrieval logic
22
- │ ├── submit_payment.ts # Payment submission logic
23
- └── lib.ts # Utility functions
24
- ├── Dockerfile # Docker configuration
25
- ├── .env # Environment variables (with generated keys)
26
- ├── .env.example # Example environment file
27
- ├── .eslintrc.json # ESLint configuration
28
- ├── .gitignore # Git ignore rules
29
- ├── package.json # Project dependencies
30
- └── tsconfig.json # TypeScript configuration
18
+ │ ├── index.ts # Entry point
19
+ │ ├── service.ts # Phase 2: ProviderService handlers
20
+ │ ├── payment_intent_pay_in_service.ts # Phase 3A: PayInProviderService handler
21
+ │ ├── payment_intent_beneficiary_service.ts # Phase 3B: BeneficiaryService handler
22
+ │ ├── publish_quotes.ts # Phase 1: payout quote publishing
23
+ ├── get_quote.ts # Phase 1: quote retrieval
24
+ ├── publish_payment_intent_quotes.ts # Phase 3A: pay-in quote publishing
25
+ ├── get_payment_intent_quote.ts # Phase 3B: indicative quote retrieval
26
+ ├── create_payment_intent.ts # Phase 3B: create a payment intent
27
+ ├── confirm_funds_received.ts # Phase 3A: confirm funds received
28
+ ├── submit_payment.ts # Phase 2: payment submission
29
+ │ └── lib.ts # Utility functions
30
+ ├── Dockerfile # Docker configuration
31
+ ├── .env # Environment variables (with generated keys)
32
+ ├── .env.example # Example environment file
33
+ ├── .eslintrc.json # ESLint configuration
34
+ ├── .gitignore # Git ignore rules
35
+ ├── package.json # Project dependencies
36
+ └── tsconfig.json # TypeScript configuration
31
37
  ```
32
38
 
33
39
  ## Key Files to Modify
@@ -64,6 +70,26 @@ your-project-name/
64
70
  4. Test payment submission by uncommenting the `submitPayment` call in `src/index.ts`.
65
71
  5. Coordinate with the T-0 team to test end-to-end payment flows.
66
72
 
73
+ ### Phase 3: Payment Intent Flow
74
+
75
+ The payment intent flow is independent of Phase 2. It is an asynchronous pay-in flow where an end-user pays a pay-in provider in fiat (bank transfer, mobile money, etc.) and a beneficiary provider receives settlement on the crypto side. Quotes are indicative until funds are received, settlement happens periodically, and a confirmation code links the end-user's payment back to a specific payment intent.
76
+
77
+ Implement **one** of the two sub-phases below depending on your role. If you participate on both sides, implement both.
78
+
79
+ **Phase 3A -- Pay-In Provider role** (skip if you're a beneficiary):
80
+
81
+ 1. **Step 3A.1** Replace the sample pay-in quote publishing in `src/publish_payment_intent_quotes.ts` with your own.
82
+ 2. **Step 3A.2** Implement `getPaymentDetails` in `src/payment_intent_pay_in_service.ts` -- return bank account / mobile money details plus a payment reference the end-user will include in their transfer.
83
+ 3. **Step 3A.3** When you detect the end-user's fiat payment, call `confirmFundsReceived` (see `src/confirm_funds_received.ts`).
84
+
85
+ **Phase 3B -- Beneficiary Provider role** (skip if you're pay-in):
86
+
87
+ 1. **Step 3B.1** Verify indicative quotes are returned (`src/get_payment_intent_quote.ts`).
88
+ 2. **Step 3B.2** Create payment intents for your end-users via `createPaymentIntent` (see `src/create_payment_intent.ts`).
89
+ 3. **Step 3B.3** Implement `paymentIntentUpdate` in `src/payment_intent_beneficiary_service.ts` to receive notifications when funds are received.
90
+
91
+ If you only play one role, delete the files for the other role and remove the corresponding `r.service(...)` registration in `src/index.ts`.
92
+
67
93
  ## Available Commands
68
94
 
69
95
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t-0/provider-starter-ts",
3
- "version": "1.1.13",
3
+ "version": "1.1.15",
4
4
  "description": "CLI tool to scaffold a Node.js t-0 Network integration service",
5
5
  "main": "dist/create.js",
6
6
  "bin": {
@@ -12,7 +12,7 @@
12
12
  "author": "",
13
13
  "license": "MIT",
14
14
  "dependencies": {
15
- "@t-0/provider-sdk": "^1.1.13",
15
+ "@t-0/provider-sdk": "^1.1.15",
16
16
  "dotenv": "^16.3.1",
17
17
  "tiny-invariant": "^1.3.3"
18
18
  },
@@ -0,0 +1,31 @@
1
+ import {type Client, PaymentIntentNetwork, PaymentMethodType} from "@t-0/provider-sdk";
2
+
3
+ export default async function confirmFundsReceived(
4
+ paymentIntentClient: Client<typeof PaymentIntentNetwork.PaymentIntentService>,
5
+ paymentIntentId: bigint,
6
+ confirmationCode: string,
7
+ transactionReference: string,
8
+ ) {
9
+ // Pay-In Provider role — Step 3A.3.
10
+ // Call this after you have matched an incoming fiat payment to a payment intent
11
+ // (using the payment reference you returned from getPaymentDetails). Settlement
12
+ // with the beneficiary provider will proceed once this confirmation is accepted.
13
+ const response = await paymentIntentClient.confirmFundsReceived({
14
+ paymentIntentId,
15
+ confirmationCode,
16
+ paymentMethod: PaymentMethodType.SEPA,
17
+ transactionReference,
18
+ // optional: if your provider has multiple legal entities, set originatorProviderLegalEntityId
19
+ })
20
+
21
+ switch (response.Result.case) {
22
+ case 'accept':
23
+ console.log(`Funds accepted for payment intent ${paymentIntentId}`)
24
+ break;
25
+ case 'reject':
26
+ console.log(`Funds rejected for payment intent ${paymentIntentId}: ${response.Result.value.reason}`)
27
+ break;
28
+ default:
29
+ console.error("unexpected result type")
30
+ }
31
+ }
@@ -0,0 +1,37 @@
1
+ import {type Client, PaymentIntentNetwork} from "@t-0/provider-sdk";
2
+ import {toProtoDecimal} from "./lib";
3
+ import {randomUUID} from "node:crypto";
4
+
5
+ export default async function createPaymentIntent(
6
+ paymentIntentClient: Client<typeof PaymentIntentNetwork.PaymentIntentService>,
7
+ ) {
8
+ // Beneficiary Provider role — Step 3B.2.
9
+ // Store the returned paymentIntentId to correlate with the PaymentIntentUpdate
10
+ // notification you'll receive on your BeneficiaryService handler once the end-user
11
+ // completes the pay-in.
12
+ const response = await paymentIntentClient.createPaymentIntent({
13
+ externalReference: randomUUID(), // idempotency key — reuse to retry without duplicating the intent
14
+ currency: 'EUR',
15
+ amount: toProtoDecimal(500, 0), // end-user pays 500 EUR
16
+ travelRuleData: {
17
+ // TODO: populate real IVMS101 beneficiary information for your end-user.
18
+ beneficiary: [{}],
19
+ },
20
+ })
21
+
22
+ switch (response.Result.case) {
23
+ case 'success':
24
+ console.log(
25
+ `Created payment intent id=${response.Result.value.paymentIntentId}`,
26
+ `with ${response.Result.value.payInDetails.length} pay-in option(s)`,
27
+ )
28
+ // TODO: persist (paymentIntentId, externalReference) and present the
29
+ // payInDetails options to your end-user.
30
+ break;
31
+ case 'failure':
32
+ console.log(`Failed to create payment intent: ${response.Result.value.reason}`)
33
+ break;
34
+ default:
35
+ console.error("unexpected result type")
36
+ }
37
+ }
@@ -0,0 +1,28 @@
1
+ import {type Client, PaymentIntentNetwork} from "@t-0/provider-sdk";
2
+ import {toProtoDecimal} from "./lib";
3
+
4
+ export default async function getPaymentIntentQuote(
5
+ paymentIntentClient: Client<typeof PaymentIntentNetwork.PaymentIntentService>,
6
+ ) {
7
+ // Beneficiary Provider role — Step 3B.1.
8
+ // Use this to check available rates before creating a payment intent. The actual
9
+ // settlement rate is determined when the pay-in provider confirms funds received.
10
+ const response = await paymentIntentClient.getQuote({
11
+ currency: "EUR",
12
+ amount: toProtoDecimal(500, 0), // end-user pays 500 EUR
13
+ })
14
+
15
+ switch (response.Result.case) {
16
+ case 'success':
17
+ console.log(
18
+ `Got ${response.Result.value.bestQuotes.length} best pay-in quotes`,
19
+ `and ${response.Result.value.allQuotes.length} total quotes`,
20
+ )
21
+ break;
22
+ case 'quoteNotFound':
23
+ console.log("No pay-in quotes available for this currency/amount")
24
+ break;
25
+ default:
26
+ console.error("unexpected result type")
27
+ }
28
+ }
@@ -4,6 +4,9 @@ import {
4
4
  createService,
5
5
  NetworkService,
6
6
  nodeAdapter,
7
+ PaymentIntentBeneficiary,
8
+ PaymentIntentNetwork,
9
+ PaymentIntentPayInProvider,
7
10
  ProviderService,
8
11
  signatureValidation,
9
12
  } from "@t-0/provider-sdk";
@@ -14,6 +17,12 @@ import CreateProviderService from "./service";
14
17
  import getQuote from "./get_quote";
15
18
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
16
19
  import submitPayment from "./submit_payment";
20
+ import CreatePayInProviderService from "./payment_intent_pay_in_service";
21
+ import CreateBeneficiaryService from "./payment_intent_beneficiary_service";
22
+ import publishPaymentIntentQuotes from "./publish_payment_intent_quotes";
23
+ import getPaymentIntentQuote from "./get_payment_intent_quote";
24
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
25
+ import createPaymentIntent from "./create_payment_intent";
17
26
 
18
27
  dotenv.config();
19
28
 
@@ -32,14 +41,22 @@ async function main() {
32
41
  console.log(`📡 Port: ${port}`);
33
42
  console.log(`🔑 Network Public Key: ${networkPublicKeyHex}`);
34
43
  const networkClient = createClient(privateKeyHex!, endpoint, NetworkService);
44
+ const paymentIntentClient = createClient(privateKeyHex!, endpoint, PaymentIntentNetwork.PaymentIntentService);
35
45
 
36
46
  await publishQuotes(networkClient, quotePublishingInterval)
37
47
 
48
+ // Phase 3A — Pay-In Provider role. Comment out if you are only a beneficiary.
49
+ await publishPaymentIntentQuotes(paymentIntentClient, quotePublishingInterval)
50
+
38
51
  const server = http.createServer(
39
52
  signatureValidation(
40
53
  nodeAdapter(
41
54
  createService(networkPublicKeyHex!, (r) => {
42
55
  r.service(ProviderService, CreateProviderService(networkClient));
56
+ // Phase 3A — Pay-In Provider role. Remove if you are only a beneficiary.
57
+ r.service(PaymentIntentPayInProvider.PayInProviderService, CreatePayInProviderService(paymentIntentClient));
58
+ // Phase 3B — Beneficiary Provider role. Remove if you are only a pay-in provider.
59
+ r.service(PaymentIntentBeneficiary.BeneficiaryService, CreateBeneficiaryService());
43
60
  })))
44
61
  ).listen(port);
45
62
  console.log("✅ Service ready and is listening at", server.address());
@@ -59,10 +76,21 @@ async function main() {
59
76
  // await submitPayment(networkClient)
60
77
 
61
78
  // TODO: Step 2.5 ask t-0 team to submit a payment which would trigger your payOut endpoint
79
+
80
+ // ──────────────────────────────────────────────────────────────
81
+ // Payment Intent Flow — Phase 3
82
+ //
83
+ // Implement the role that applies to you. See the README for details.
84
+ // ──────────────────────────────────────────────────────────────
85
+
86
+ // Phase 3B — Beneficiary Provider role. Comment out if you are only a pay-in provider.
87
+ // TODO: Step 3B.1 check that indicative quotes are returned
88
+ await getPaymentIntentQuote(paymentIntentClient)
89
+ // TODO: Step 3B.2 create a payment intent for a real end-user when they want to pay
90
+ // await createPaymentIntent(paymentIntentClient)
62
91
  }
63
92
 
64
93
  main().catch((error) => {
65
94
  console.error('❌ Error starting service:', error);
66
95
  process.exit(1);
67
96
  });
68
-
@@ -0,0 +1,38 @@
1
+ import {
2
+ HandlerContext,
3
+ PaymentIntentBeneficiary,
4
+ } from "@t-0/provider-sdk";
5
+
6
+ /*
7
+ Payment Intent Flow — Beneficiary Provider role.
8
+
9
+ Implement this service if you are a beneficiary provider (you receive settlement for the crypto side).
10
+ Please refer to docs and proto definition comments to understand the full flow.
11
+ */
12
+ const CreateBeneficiaryService = () => {
13
+ return {
14
+ // TODO: Step 3B.3 Implement how you handle notifications about your payment intents.
15
+ //
16
+ // The network calls this endpoint when the status of one of your payment intents
17
+ // changes (e.g. funds received from the end-user). Correlate req.paymentIntentId
18
+ // with the id you stored after calling createPaymentIntent and update your
19
+ // internal state accordingly.
20
+ async paymentIntentUpdate(req: PaymentIntentBeneficiary.PaymentIntentUpdateRequest, _: HandlerContext) {
21
+ switch (req.update.case) {
22
+ case 'fundsReceived':
23
+ console.log(
24
+ `Payment intent ${req.paymentIntentId}: funds received,`,
25
+ `settlementAmount=${JSON.stringify(req.update.value.settlementAmount)},`,
26
+ `paymentMethod=${req.update.value.paymentMethod},`,
27
+ `transactionReference=${req.update.value.transactionReference}`,
28
+ )
29
+ break;
30
+ default:
31
+ console.log(`Payment intent ${req.paymentIntentId}: unknown update variant`)
32
+ }
33
+ return {} as PaymentIntentBeneficiary.PaymentIntentUpdateResponse
34
+ },
35
+ }
36
+ };
37
+
38
+ export default CreateBeneficiaryService;
@@ -0,0 +1,39 @@
1
+ import {
2
+ type Client,
3
+ HandlerContext,
4
+ PaymentIntentNetwork,
5
+ PaymentIntentPayInProvider,
6
+ } from "@t-0/provider-sdk";
7
+
8
+ /*
9
+ Payment Intent Flow — Pay-In Provider role.
10
+
11
+ Implement this service if you are a pay-in provider (you receive fiat from end-users).
12
+ Please refer to docs and proto definition comments to understand the full flow.
13
+ */
14
+ const CreatePayInProviderService = (paymentIntentClient: Client<typeof PaymentIntentNetwork.PaymentIntentService>) => {
15
+ return {
16
+ // TODO: Step 3A.2 Implement how you return payment details for the end-user.
17
+ //
18
+ // The network calls this endpoint during CreatePaymentIntent processing. Return
19
+ // payment details (bank account, mobile money number, etc.) for each requested
20
+ // paymentMethod, including a unique payment reference that will let you match
21
+ // the incoming fiat payment back to this payment intent.
22
+ //
23
+ // Store (paymentIntentId, confirmationCode) so you can validate it later in
24
+ // confirmFundsReceived.
25
+ async getPaymentDetails(req: PaymentIntentPayInProvider.GetPaymentDetailsRequest, _: HandlerContext) {
26
+ console.log(`Received GetPaymentDetails for payment intent ${req.paymentIntentId}, methods: ${req.paymentMethods.join(',')}`)
27
+ return {
28
+ result: {
29
+ case: 'details',
30
+ value: {
31
+ paymentDetails: [], // TODO: populate one PaymentDetails per requested paymentMethod
32
+ },
33
+ },
34
+ } as unknown as PaymentIntentPayInProvider.GetPaymentDetailsResponse
35
+ },
36
+ }
37
+ };
38
+
39
+ export default CreatePayInProviderService;
@@ -0,0 +1,39 @@
1
+ import {type Client, PaymentIntentNetwork, PaymentMethodType} from "@t-0/provider-sdk";
2
+ import {toProtoDecimal} from "./lib";
3
+ import {randomUUID} from "node:crypto";
4
+ import {timestampFromDate} from "@bufbuild/protobuf/wkt";
5
+
6
+ export default async function publishPaymentIntentQuotes(
7
+ paymentIntentClient: Client<typeof PaymentIntentNetwork.PaymentIntentService>,
8
+ quotePublishingInterval: number,
9
+ ): Promise<void> {
10
+ // TODO: Step 3A.1 replace this with fetching pay-in quotes from your systems and publishing them into t-0 Network.
11
+ // We recommend publishing at least once per 5 seconds, but not more than once per second.
12
+ const tick = async () => {
13
+ try {
14
+ // NOTE: Every updateQuote request discards all previous payment intent quotes
15
+ // that were published before. Combine multiple quotes into a single request.
16
+ await paymentIntentClient.updateQuote({
17
+ paymentIntentQuotes: [{
18
+ currency: 'EUR',
19
+ paymentMethod: PaymentMethodType.SEPA,
20
+ expiration: timestampFromDate(new Date(Date.now() + 30 * 1000)), // 30 seconds from now
21
+ timestamp: timestampFromDate(new Date()),
22
+ bands: [{
23
+ clientQuoteId: randomUUID(),
24
+ maxAmount: toProtoDecimal(1000, 0), // max 1000 USD for this band
25
+ // rate is always USD/XXX, so for EUR quote should be USD/EUR
26
+ rate: toProtoDecimal(92, -2), // rate 0.92
27
+ }],
28
+ }],
29
+ })
30
+ } catch (error) {
31
+ console.error(error);
32
+ return
33
+ }
34
+ console.log("payment intent quote published")
35
+ }
36
+
37
+ await tick()
38
+ setInterval(tick, quotePublishingInterval);
39
+ }
@@ -69,7 +69,7 @@ const CreateProviderService = (networkClient: Client<typeof NetworkService>) =>
69
69
  return {} as AppendLedgerEntriesResponse
70
70
  },
71
71
 
72
- async approvePaymentQuote(req: ApprovePaymentQuoteRequest, _: HandlerContext) {
72
+ async approvePaymentQuotes(req: ApprovePaymentQuoteRequest, _: HandlerContext) {
73
73
  // TODO: when the payment goes through the Manual AML Check on the pay-out provider side, the provider submitted the payment will have a last look to approve final quote
74
74
  // The request includes payOutFix — the fixed charge in USD for this payout.
75
75
  // Consider it alongside payOutRate and payOutAmount when deciding to accept.