@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 +39 -13
- package/package.json +1 -1
- package/template/package.json +1 -1
- package/template/src/confirm_funds_received.ts +31 -0
- package/template/src/create_payment_intent.ts +37 -0
- package/template/src/get_payment_intent_quote.ts +28 -0
- package/template/src/index.ts +29 -1
- package/template/src/payment_intent_beneficiary_service.ts +38 -0
- package/template/src/payment_intent_pay_in_service.ts +39 -0
- package/template/src/publish_payment_intent_quotes.ts +39 -0
- package/template/src/service.ts +1 -1
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
|
|
19
|
-
│ ├── service.ts
|
|
20
|
-
│ ├──
|
|
21
|
-
│ ├──
|
|
22
|
-
│ ├──
|
|
23
|
-
│
|
|
24
|
-
├──
|
|
25
|
-
├── .
|
|
26
|
-
├── .
|
|
27
|
-
├── .
|
|
28
|
-
├── .
|
|
29
|
-
|
|
30
|
-
|
|
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
package/template/package.json
CHANGED
|
@@ -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
|
+
}
|
package/template/src/index.ts
CHANGED
|
@@ -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
|
+
}
|
package/template/src/service.ts
CHANGED
|
@@ -69,7 +69,7 @@ const CreateProviderService = (networkClient: Client<typeof NetworkService>) =>
|
|
|
69
69
|
return {} as AppendLedgerEntriesResponse
|
|
70
70
|
},
|
|
71
71
|
|
|
72
|
-
async
|
|
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.
|