diff --git a/.eslintrc.js b/.eslintrc.js index 12e32cdb..0c22a8c9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,7 +10,7 @@ module.exports = { ...spellcheckerRule, cspell: { ...cspellConfig, - ignoreWords: ['unmarshal', 'JUvpllMEYUZ2joO59UNui_XYDqxVqiFLLAJ8klWuPBw', 'gdwj', 'fwor'] + ignoreWords: ['unmarshal', 'JUvpllMEYUZ2joO59UNui_XYDqxVqiFLLAJ8klWuPBw', 'gdwj', 'fwor', 'multichain'] } } ] diff --git a/src/iden3comm/handlers/payment.ts b/src/iden3comm/handlers/payment.ts index 1d980e7e..e43f3d8d 100644 --- a/src/iden3comm/handlers/payment.ts +++ b/src/iden3comm/handlers/payment.ts @@ -8,9 +8,12 @@ import { proving } from '@iden3/js-jwz'; import { byteEncoder } from '../../utils'; import { AbstractMessageHandler, IProtocolMessageHandler } from './message-handler'; import { - PaymentInfo, + Iden3PaymentCryptoV1, + Iden3PaymentRailsRequestV1, + Iden3PaymentRailsResponseV1, + Iden3PaymentRequestCryptoV1, PaymentMessage, - PaymentRequestDataInfo, + PaymentMessageBody, PaymentRequestInfo, PaymentRequestMessage } from '../types/protocol/payment'; @@ -52,10 +55,14 @@ export function createPaymentRequest( * createPayment is a function to create protocol payment message * @param {DID} sender - sender did * @param {DID} receiver - receiver did - * @param {PaymentInfo[]} payments - payments + * @param {PaymentMessageBody} body - payments * @returns `PaymentMessage` */ -export function createPayment(sender: DID, receiver: DID, payments: PaymentInfo[]): PaymentMessage { +export function createPayment( + sender: DID, + receiver: DID, + body: PaymentMessageBody +): PaymentMessage { const uuidv4 = uuid.v4(); const request: PaymentMessage = { id: uuidv4, @@ -64,9 +71,7 @@ export function createPayment(sender: DID, receiver: DID, payments: PaymentInfo[ to: receiver.string(), typ: MediaType.PlainMessage, type: PROTOCOL_MESSAGE_TYPE.PAYMENT_MESSAGE_TYPE, - body: { - payments - } + body }; return request; } @@ -110,13 +115,19 @@ export interface IPaymentHandler { /** @beta PaymentRequestMessageHandlerOptions represents payment-request handler options */ export type PaymentRequestMessageHandlerOptions = { - paymentHandler: (data: PaymentRequestDataInfo) => Promise; + paymentHandler: ( + data: Iden3PaymentRequestCryptoV1 | Iden3PaymentRailsRequestV1 + ) => Promise; + multichainSelectedChainId?: string; }; /** @beta PaymentHandlerOptions represents payment handler options */ export type PaymentHandlerOptions = { paymentRequest: PaymentRequestMessage; - paymentValidationHandler: (txId: string, data: PaymentRequestDataInfo) => Promise; + paymentValidationHandler: ( + txId: string, + data: Iden3PaymentRequestCryptoV1 | Iden3PaymentRailsRequestV1 + ) => Promise; }; /** @beta PaymentHandlerParams represents payment handler params */ @@ -202,13 +213,54 @@ export class PaymentHandler const senderDID = DID.parse(paymentRequest.to); const receiverDID = DID.parse(paymentRequest.from); - const payments: PaymentInfo[] = []; + const payments: (Iden3PaymentCryptoV1 | Iden3PaymentRailsResponseV1)[] = []; for (let i = 0; i < paymentRequest.body.payments.length; i++) { const paymentReq = paymentRequest.body.payments[i]; if (paymentReq.type !== PaymentRequestType.PaymentRequest) { throw new Error(`failed request. not supported '${paymentReq.type}' payment type `); } + // if multichain request + if (Array.isArray(paymentReq.data)) { + if (!ctx.multichainSelectedChainId) { + throw new Error(`failed request. no selected chain id`); + } + + const selectedPayment = paymentReq.data.find((p) => { + const proofs = Array.isArray(p.proof) ? p.proof : [p.proof]; + const eip712Signature2021Proof = proofs.filter( + (p) => p.type === 'EthereumEip712Signature2021' + )[0]; + if (!eip712Signature2021Proof) { + return false; + } + return eip712Signature2021Proof.eip712.domain.chainId === ctx.multichainSelectedChainId; + }); + + if (!selectedPayment) { + throw new Error( + `failed request. no payment in request for chain id ${ctx.multichainSelectedChainId}` + ); + } + + if (selectedPayment.type !== PaymentRequestDataType.Iden3PaymentRailsRequestV1) { + throw new Error(`failed request. not supported '${selectedPayment.type}' payment type `); + } + + const txId = await ctx.paymentHandler(selectedPayment); + + payments.push({ + nonce: selectedPayment.nonce, + type: PaymentType.Iden3PaymentRailsResponseV1, + paymentData: { + txId, + chainId: ctx.multichainSelectedChainId + } + }); + + continue; + } + if (paymentReq.data.type !== PaymentRequestDataType.Iden3PaymentRequestCryptoV1) { throw new Error(`failed request. not supported '${paymentReq.data.type}' payment type `); } @@ -224,7 +276,7 @@ export class PaymentHandler }); } - const paymentMessage = createPayment(senderDID, receiverDID, payments); + const paymentMessage = createPayment(senderDID, receiverDID, { payments }); const response = await this.packMessage(paymentMessage, senderDID); const agentResult = await fetch(paymentRequest.body.agent, { @@ -291,14 +343,43 @@ export class PaymentHandler for (let i = 0; i < payment.body.payments.length; i++) { const p = payment.body.payments[i]; - const paymentRequestData = opts.paymentRequest.body.payments.find((r) => r.data.id === p.id); - if (!paymentRequestData) { - throw new Error(`can't find payment request for payment id ${p.id}`); + let data: Iden3PaymentRequestCryptoV1 | Iden3PaymentRailsRequestV1 | undefined; + switch (p.type) { + case PaymentType.Iden3PaymentCryptoV1: { + data = opts.paymentRequest.body.payments.find( + (r) => (r.data as Iden3PaymentRequestCryptoV1).id === p.id + )?.data as Iden3PaymentRequestCryptoV1; + if (!data) { + throw new Error(`can't find payment request for payment id ${p.id}`); + } + break; + } + case PaymentType.Iden3PaymentRailsResponseV1: { + for (let j = 0; j < opts.paymentRequest.body.payments.length; j++) { + const paymentReq = opts.paymentRequest.body.payments[j]; + if (Array.isArray(paymentReq.data)) { + const selectedPayment = paymentReq.data.find( + (r) => (r as Iden3PaymentRailsRequestV1).nonce === p.nonce + ) as Iden3PaymentRailsRequestV1; + if (selectedPayment) { + data = selectedPayment; + break; + } + } + } + + if (!data) { + throw new Error(`can't find payment request for payment nonce ${p.nonce}`); + } + break; + } + default: + throw new Error(`failed request. not supported '${p.type}' payment type `); } if (!opts.paymentValidationHandler) { throw new Error(`please provide payment validation handler in options`); } - await opts.paymentValidationHandler(p.paymentData.txId, paymentRequestData.data); + await opts.paymentValidationHandler(p.paymentData.txId, data); } } diff --git a/src/iden3comm/types/protocol/payment.ts b/src/iden3comm/types/protocol/payment.ts index f12a3bd4..5692cf33 100644 --- a/src/iden3comm/types/protocol/payment.ts +++ b/src/iden3comm/types/protocol/payment.ts @@ -1,10 +1,5 @@ import { BasicMessage } from '../'; -import { - PaymentRequestDataType, - PaymentRequestType, - PaymentType, - SupportedCurrencies -} from '../../../verifiable'; +import { PaymentRequestType, SupportedCurrencies } from '../../../verifiable'; import { PROTOCOL_MESSAGE_TYPE } from '../../constants'; /** @beta PaymentRequestMessage is struct the represents payment-request message */ @@ -26,14 +21,14 @@ export type PaymentRequestInfo = { context: string; }[]; type: PaymentRequestType; - data: PaymentRequestDataInfo; + data: Iden3PaymentRequestCryptoV1 | Iden3PaymentRailsRequestV1[]; expiration?: string; description?: string; }; -/** @beta PaymentRequestDataInfo is struct the represents payment data info for payment-request */ -export type PaymentRequestDataInfo = { - type: PaymentRequestDataType; +/** @beta Iden3PaymentRequestCryptoV1 is struct the represents payment data info for payment-request */ +export type Iden3PaymentRequestCryptoV1 = { + type: 'Iden3PaymentRequestCryptoV1'; amount: string; id: string; chainId: string; @@ -42,6 +37,35 @@ export type PaymentRequestDataInfo = { signature?: string; }; +export type Iden3PaymentRailsRequestV1 = { + type: 'Iden3PaymentRailsRequestV1'; + recipient: string; + value: string; + expirationDate: string; + nonce: string; + metadata: string; + proof: EthereumEip712Signature2021 | EthereumEip712Signature2021[]; +}; + +export type EthereumEip712Signature2021 = { + type: 'EthereumEip712Signature2021'; + proofPurpose: string; + proofValue: string; + verificationMethod: string; + created: string; + eip712: { + types: string; + primaryType: string; + domain: { + name: string; + version: string; + chainId: string; + verifyingContract: string; + salt: string; + }; + }; +}; + /** @beta PaymentMessage is struct the represents payment message */ export type PaymentMessage = BasicMessage & { body: PaymentMessageBody; @@ -50,14 +74,24 @@ export type PaymentMessage = BasicMessage & { /** @beta PaymentMessageBody is struct the represents body for payment message */ export type PaymentMessageBody = { - payments: PaymentInfo[]; + payments: (Iden3PaymentCryptoV1 | Iden3PaymentRailsResponseV1)[]; }; -/** @beta PaymentInfo is struct the represents payment info for payment */ -export type PaymentInfo = { +/** @beta Iden3PaymentCryptoV1 is struct the represents payment info for payment */ +export type Iden3PaymentCryptoV1 = { id: string; - type: PaymentType; + type: 'Iden3PaymentCryptoV1'; + paymentData: { + txId: string; + }; +}; + +/** @beta Iden3PaymentRailsResponseV1 is struct the represents payment info for Iden3PaymentRailsRequestV1 */ +export type Iden3PaymentRailsResponseV1 = { + nonce: string; + type: 'Iden3PaymentRailsResponseV1'; paymentData: { txId: string; + chainId: string; }; }; diff --git a/src/verifiable/constants.ts b/src/verifiable/constants.ts index 53e1a247..232f5aea 100644 --- a/src/verifiable/constants.ts +++ b/src/verifiable/constants.ts @@ -128,7 +128,8 @@ export enum PaymentRequestType { * @enum {string} */ export enum PaymentRequestDataType { - Iden3PaymentRequestCryptoV1 = 'Iden3PaymentRequestCryptoV1' + Iden3PaymentRequestCryptoV1 = 'Iden3PaymentRequestCryptoV1', + Iden3PaymentRailsRequestV1 = 'Iden3PaymentRailsRequestV1' } /** @@ -137,7 +138,8 @@ export enum PaymentRequestDataType { * @enum {string} */ export enum PaymentType { - Iden3PaymentCryptoV1 = 'Iden3PaymentCryptoV1' + Iden3PaymentCryptoV1 = 'Iden3PaymentCryptoV1', + Iden3PaymentRailsResponseV1 = 'Iden3PaymentRailsResponseV1' } /** diff --git a/tests/handlers/payment.test.ts b/tests/handlers/payment.test.ts index b2196f46..d101d153 100644 --- a/tests/handlers/payment.test.ts +++ b/tests/handlers/payment.test.ts @@ -42,7 +42,8 @@ import { PaymentHandler } from '../../src/iden3comm/handlers/payment'; import { - PaymentRequestDataInfo, + Iden3PaymentRailsRequestV1, + Iden3PaymentRequestCryptoV1, PaymentRequestInfo } from '../../src/iden3comm/types/protocol/payment'; import { Contract, ethers, JsonRpcProvider } from 'ethers'; @@ -99,25 +100,30 @@ describe('payment-request handler', () => { const paymentIntegrationHandlerFunc = (sessionId: string, did: string) => - async (data: PaymentRequestDataInfo): Promise => { + async (data: Iden3PaymentRequestCryptoV1 | Iden3PaymentRailsRequestV1): Promise => { + const iden3PaymentRequestCryptoV1 = data as Iden3PaymentRequestCryptoV1; const rpcProvider = new JsonRpcProvider(RPC_URL); const ethSigner = new ethers.Wallet(WALLET_KEY, rpcProvider); - const payContract = new Contract(data.address, payContractAbi, ethSigner); - if (data.currency !== SupportedCurrencies.ETH) { + const payContract = new Contract( + iden3PaymentRequestCryptoV1.address, + payContractAbi, + ethSigner + ); + if (iden3PaymentRequestCryptoV1.currency !== SupportedCurrencies.ETH) { throw new Error('integration can only pay in eth currency'); } - const options = { value: ethers.parseUnits(data.amount, 'ether') }; + const options = { value: ethers.parseUnits(iden3PaymentRequestCryptoV1.amount, 'ether') }; const txData = await payContract.pay(sessionId, did, options); return txData.hash; }; const paymentValidationIntegrationHandlerFunc = async ( txId: string, - data: PaymentRequestDataInfo + data: Iden3PaymentRequestCryptoV1 | Iden3PaymentRailsRequestV1 ): Promise => { const rpcProvider = new JsonRpcProvider(RPC_URL); const tx = await rpcProvider.getTransaction(txId); - if (tx?.value !== ethers.parseUnits(data.amount, 'ether')) { + if (tx?.value !== ethers.parseUnits((data as Iden3PaymentRequestCryptoV1).amount, 'ether')) { throw new Error('invalid value'); } }; @@ -230,15 +236,17 @@ describe('payment-request handler', () => { it('payment handler', async () => { const paymentRequest = createPaymentRequest(issuerDID, userDID, agent, [paymentReqInfo]); - const payment = createPayment(userDID, issuerDID, [ - { - id: paymentRequest.body.payments[0].data.id, - type: PaymentType.Iden3PaymentCryptoV1, - paymentData: { - txId: '0x312312334' + const payment = createPayment(userDID, issuerDID, { + payments: [ + { + id: (paymentRequest.body.payments[0].data as Iden3PaymentRequestCryptoV1).id, + type: PaymentType.Iden3PaymentCryptoV1, + paymentData: { + txId: '0x312312334' + } } - } - ]); + ] + }); await paymentHandler.handlePayment(payment, { paymentRequest, @@ -270,15 +278,17 @@ describe('payment-request handler', () => { it.skip('payment handler (integration test)', async () => { const paymentRequest = createPaymentRequest(issuerDID, userDID, agent, [paymentReqInfo]); - const payment = createPayment(userDID, issuerDID, [ - { - id: paymentRequest.body.payments[0].data.id, - type: PaymentType.Iden3PaymentCryptoV1, - paymentData: { - txId: '0xe9bea8e7adfe1092a8a4ca2cd75f4d21cc54b9b7a31bd8374b558d11b58a6a1a' + const payment = createPayment(userDID, issuerDID, { + payments: [ + { + id: (paymentRequest.body.payments[0].data as Iden3PaymentRequestCryptoV1).id, + type: PaymentType.Iden3PaymentCryptoV1, + paymentData: { + txId: '0xe9bea8e7adfe1092a8a4ca2cd75f4d21cc54b9b7a31bd8374b558d11b58a6a1a' + } } - } - ]); + ] + }); await paymentHandler.handlePayment(payment, { paymentRequest,