UNPKG

@0xpolygonid/js-sdk

Version:
723 lines (670 loc) 24.4 kB
import { PROTOCOL_MESSAGE_TYPE } from '../constants'; import { MediaType } from '../constants'; import { BasicMessage, IPackageManager, PackerParams } from '../types'; import { DID, getUnixTimestamp } from '@iden3/js-iden3-core'; import * as uuid from 'uuid'; import { buildSolanaPayment, byteEncoder, verifyIden3SolanaPaymentRequest } from '../../utils'; import { AbstractMessageHandler, BasicHandlerOptions, IProtocolMessageHandler, defaultProvingMethodAlg, getProvingMethodAlgFromJWZ } from './message-handler'; import { Iden3PaymentCryptoV1, Iden3PaymentRailsERC20RequestV1, Iden3PaymentRailsERC20V1, Iden3PaymentRailsRequestV1, Iden3PaymentRailsSolanaRequestV1, Iden3PaymentRailsSolanaSPLRequestV1, Iden3PaymentRailsSolanaSPLV1, Iden3PaymentRailsSolanaV1, Iden3PaymentRailsV1, Iden3PaymentRequestCryptoV1, MultiChainPaymentConfig, PaymentMessage, PaymentRequestInfo, PaymentRequestMessage, PaymentRequestTypeUnion, PaymentTypeUnion } from '../types/protocol/payment'; import { PaymentFeatures, PaymentRequestDataType, PaymentType } from '../../verifiable'; import { Signer } from 'ethers'; import { Resolvable } from 'did-resolver'; import { HandlerPackerParams, initDefaultPackerOptions, verifyExpiresTime } from './common'; import { Keypair } from '@solana/web3.js'; import { buildEvmPayment, verifyEIP712TypedData } from '../../utils/payments/evm'; /** @beta PaymentRequestCreationOptions represents payment-request creation options */ export type PaymentRequestCreationOptions = { expires_time?: Date; }; /** @beta PaymentCreationOptions represents payment creation options */ export type PaymentCreationOptions = { expires_time?: Date; }; /** @beta CreatePaymentRailsV1Options holds options for PaymentRailsV1 creation */ export type CreatePaymentRailsV1Options = { ethSigner?: Signer; solSigner?: Keypair; }; /** * @beta * createPaymentRequest is a function to create protocol payment-request message * @param {DID} sender - sender did * @param {DID} receiver - receiver did * @param {string} agent - agent URL * @param {PaymentRequestInfo[]} payments - payments * @returns `PaymentRequestMessage` */ export function createPaymentRequest( sender: DID, receiver: DID, agent: string, payments: PaymentRequestInfo[], opts?: PaymentRequestCreationOptions ): PaymentRequestMessage { const uuidv4 = uuid.v4(); const request: PaymentRequestMessage = { id: uuidv4, thid: uuidv4, from: sender.string(), to: receiver.string(), typ: MediaType.PlainMessage, type: PROTOCOL_MESSAGE_TYPE.PAYMENT_REQUEST_MESSAGE_TYPE, body: { agent, payments }, created_time: getUnixTimestamp(new Date()), expires_time: opts?.expires_time ? getUnixTimestamp(opts.expires_time) : undefined }; return request; } /** * @beta * PaymentRailsInfo represents payment info for payment rails */ export type PaymentRailsInfo = { credentials: { type: string; context: string; }[]; description?: string; options: PaymentRailsOptionInfo[]; }; /** * @beta * PaymentRailsOptionInfo represents option info for payment rails */ export type PaymentRailsOptionInfo = { optionId: string; chainId: string; nonce: bigint; amount: string; expirationDate?: Date; }; /** * @beta * createPayment is a function to create protocol payment message * @param {DID} sender - sender did * @param {DID} receiver - receiver did * @param {PaymentMessageBody} body - payments * @returns `PaymentMessage` */ export function createPayment( sender: DID, receiver: DID, payments: PaymentTypeUnion[], opts?: PaymentCreationOptions ): PaymentMessage { const uuidv4 = uuid.v4(); const request: PaymentMessage = { id: uuidv4, thid: uuidv4, from: sender.string(), to: receiver.string(), typ: MediaType.PlainMessage, type: PROTOCOL_MESSAGE_TYPE.PAYMENT_MESSAGE_TYPE, body: { payments }, created_time: getUnixTimestamp(new Date()), expires_time: opts?.expires_time ? getUnixTimestamp(opts.expires_time) : undefined }; return request; } /** * @beta * Interface that allows the processing of the payment-request and payment protocol messages * * @interface IPaymentHandler */ export interface IPaymentHandler { /** * @beta * unpacks payment-request * @param {Uint8Array} request - raw byte message * @returns `Promise<PaymentRequestMessage>` */ parsePaymentRequest(request: Uint8Array): Promise<PaymentRequestMessage>; /** * @beta * handle payment-request * @param {Uint8Array} request - raw byte message * @param {PaymentRequestMessageHandlerOptions} opts - handler options * @returns {Promise<Uint8Array>} - agent message or null */ handlePaymentRequest( request: Uint8Array, opts: PaymentRequestMessageHandlerOptions ): Promise<Uint8Array | null>; /** * @beta * handle payment protocol message * @param {PaymentMessage} payment - payment message * @param {PaymentHandlerOptions} opts - options * @returns `Promise<void>` */ handlePayment(payment: PaymentMessage, opts: PaymentHandlerOptions): Promise<void>; /** * @beta * createPaymentRailsV1 is a function to create protocol payment message * @param {DID} sender - sender did * @param {DID} receiver - receiver did * @param {string} agent - agent URL * @param {Signer} signer - ETH signer * @param payments - payment options * @param {CreatePaymentRailsV1Options} createOptions - options for payment rails creation * @returns {Promise<PaymentRequestMessage>} */ createPaymentRailsV1( sender: DID, receiver: DID, agent: string, signer: Signer, // the same as createOptions.ethSigner (for compatibility) payments: PaymentRailsInfo[], createOptions?: CreatePaymentRailsV1Options ): Promise<PaymentRequestMessage>; } /** @beta PaymentRequestMessageHandlerOptions represents payment-request handler options */ export type PaymentRequestMessageHandlerOptions = BasicHandlerOptions & { paymentHandler: (data: PaymentRequestTypeUnion) => Promise<string>; /* selected payment nonce (for Iden3PaymentRequestCryptoV1 type it should be equal to Payment id field) */ nonce: string; erc20TokenApproveHandler?: (data: Iden3PaymentRailsERC20RequestV1) => Promise<string>; packerOptions?: HandlerPackerParams; mediaType?: MediaType; }; /** @beta PaymentHandlerOptions represents payment handler options */ export type PaymentHandlerOptions = BasicHandlerOptions & { paymentRequest: PaymentRequestMessage; paymentValidationHandler: (txId: string, data: PaymentRequestTypeUnion) => Promise<void>; }; /** @beta PaymentHandlerParams represents payment handler params */ export type PaymentHandlerParams = { packerParams: PackerParams; documentResolver: Resolvable; multiChainPaymentConfig?: MultiChainPaymentConfig[]; /* * allowed signers for payment request (if not provided, any signer is allowed) */ allowedSigners?: string[]; }; /** * * Allows to process PaymentRequest protocol message * @beta * @class PaymentHandler * @implements implements IPaymentHandler interface */ export class PaymentHandler extends AbstractMessageHandler implements IPaymentHandler, IProtocolMessageHandler { /** * @beta Creates an instance of PaymentHandler. * @param {IPackageManager} _packerMgr - package manager to unpack message envelope * @param {PaymentHandlerParams} _params - payment handler params * */ constructor( private readonly _packerMgr: IPackageManager, private readonly _params: PaymentHandlerParams ) { super(); } public async handle( message: BasicMessage, context: PaymentRequestMessageHandlerOptions | PaymentHandlerOptions ): Promise<BasicMessage | null> { switch (message.type) { case PROTOCOL_MESSAGE_TYPE.PAYMENT_REQUEST_MESSAGE_TYPE: return await this.handlePaymentRequestMessage( message as PaymentRequestMessage, context as PaymentRequestMessageHandlerOptions ); case PROTOCOL_MESSAGE_TYPE.PAYMENT_MESSAGE_TYPE: await this.handlePayment(message as PaymentMessage, context as PaymentHandlerOptions); return null; default: return super.handle(message, context as { [key: string]: unknown }); } } /** * @inheritdoc IPaymentHandler#parsePaymentRequest */ async parsePaymentRequest(request: Uint8Array): Promise<PaymentRequestMessage> { const { unpackedMessage: message } = await this._packerMgr.unpack(request); const paymentRequest = message as PaymentRequestMessage; if (message.type !== PROTOCOL_MESSAGE_TYPE.PAYMENT_REQUEST_MESSAGE_TYPE) { throw new Error('Invalid media type'); } return paymentRequest; } private async handlePaymentRequestMessage( paymentRequest: PaymentRequestMessage, ctx: PaymentRequestMessageHandlerOptions ): Promise<BasicMessage | null> { if (!paymentRequest.to) { throw new Error(`failed request. empty 'to' field`); } if (!paymentRequest.from) { throw new Error(`failed request. empty 'from' field`); } if (!paymentRequest.body.payments?.length) { throw new Error(`failed request. no 'payments' in body`); } if (!ctx.paymentHandler) { throw new Error(`please provide payment handler in context`); } const senderDID = DID.parse(paymentRequest.to); const receiverDID = DID.parse(paymentRequest.from); const payments: PaymentTypeUnion[] = []; for (let i = 0; i < paymentRequest.body.payments.length; i++) { const { data } = paymentRequest.body.payments[i]; const selectedPayment = Array.isArray(data) ? data.find((p) => { return p.type === PaymentRequestDataType.Iden3PaymentRequestCryptoV1 ? p.id === ctx.nonce : p.nonce === ctx.nonce; }) : data; if (!selectedPayment) { throw new Error(`failed request. no payment in request for nonce ${ctx.nonce}`); } switch (selectedPayment.type) { case PaymentRequestDataType.Iden3PaymentRequestCryptoV1: payments.push( await this.handleIden3PaymentRequestCryptoV1(selectedPayment, ctx.paymentHandler) ); break; case PaymentRequestDataType.Iden3PaymentRailsRequestV1: payments.push( await this.handleIden3PaymentRailsRequestV1(selectedPayment, ctx.paymentHandler) ); break; case PaymentRequestDataType.Iden3PaymentRailsERC20RequestV1: payments.push( await this.handleIden3PaymentRailsERC20RequestV1( selectedPayment, ctx.paymentHandler, ctx.erc20TokenApproveHandler ) ); break; case PaymentRequestDataType.Iden3PaymentRailsSolanaRequestV1: payments.push( await this.handleIden3PaymentRailsSolanaRequestV1(selectedPayment, ctx.paymentHandler) ); break; case PaymentRequestDataType.Iden3PaymentRailsSolanaSPLRequestV1: payments.push( await this.handleIden3PaymentRailsSolanaSPLRequestV1( { ...selectedPayment, type: PaymentRequestDataType.Iden3PaymentRailsSolanaSPLRequestV1 }, ctx.paymentHandler ) ); break; } } const paymentMessage = createPayment(senderDID, receiverDID, payments); const mediaType = ctx?.mediaType || this._params.packerParams.mediaType || MediaType.ZKPMessage; const packerParams = initDefaultPackerOptions( mediaType, ctx?.packerOptions || this._params.packerParams, { senderDID: senderDID, provingMethodAlg: ctx.messageProvingMethodAlg } ); const response = await this.packMessage(paymentMessage, senderDID, mediaType, packerParams); const agentResult = await fetch(paymentRequest.body.agent, { method: 'POST', body: response.buffer as ArrayBuffer, headers: { ...ctx.headers, 'Content-Type': this._params.packerParams.mediaType === MediaType.PlainMessage ? 'application/json' : 'application/octet-stream' } }); const arrayBuffer = await agentResult.arrayBuffer(); if (!arrayBuffer.byteLength) { return null; } const { unpackedMessage } = await this._packerMgr.unpack(new Uint8Array(arrayBuffer)); return unpackedMessage; } /** * @inheritdoc IPaymentHandler#handlePaymentRequest */ async handlePaymentRequest( request: Uint8Array, opts: PaymentRequestMessageHandlerOptions ): Promise<Uint8Array | null> { const paymentRequest = await this.parsePaymentRequest(request); if (!paymentRequest.from) { throw new Error(`failed request. empty 'from' field`); } if (!paymentRequest.to) { throw new Error(`failed request. empty 'to' field`); } if (!opts?.allowExpiredMessages) { verifyExpiresTime(paymentRequest); } const mediaType = opts.mediaType || this._params.packerParams.mediaType || MediaType.ZKPMessage; if (!opts.packerOptions) { opts.packerOptions = this._params.packerParams.packerOptions; } opts.mediaType = mediaType; const senderDID = DID.parse(paymentRequest.to); opts.packerOptions = initDefaultPackerOptions(mediaType, opts.packerOptions, { provingMethodAlg: await getProvingMethodAlgFromJWZ(request), senderDID }); const agentMessage = await this.handlePaymentRequestMessage(paymentRequest, opts); if (!agentMessage) { return null; } return this.packMessage( agentMessage, senderDID, opts.mediaType || MediaType.ZKPMessage, opts.packerOptions ); } /** * @inheritdoc IPaymentHandler#handlePayment */ async handlePayment(payment: PaymentMessage, params: PaymentHandlerOptions) { if (!params?.allowExpiredMessages) { verifyExpiresTime(payment); } if (params.paymentRequest.from !== payment.to) { throw new Error( `sender of the request is not a target of response - expected ${params.paymentRequest.from}, given ${payment.to}` ); } if (!payment.body.payments.length) { throw new Error(`failed request. empty 'payments' field in body`); } if (!params.paymentValidationHandler) { throw new Error(`please provide payment validation handler in options`); } for (let i = 0; i < payment.body.payments.length; i++) { const p = payment.body.payments[i]; const nonce = p.type === PaymentType.Iden3PaymentCryptoV1 ? p.id : p.nonce; const requestDataArr = params.paymentRequest.body.payments .map((r) => (Array.isArray(r.data) ? r.data : [r.data])) .flat(); const requestData = requestDataArr.find((r) => r.type === PaymentRequestDataType.Iden3PaymentRequestCryptoV1 ? r.id === nonce : r.nonce === nonce ); if (!requestData) { throw new Error( `can't find payment request for payment ${ p.type === PaymentType.Iden3PaymentCryptoV1 ? 'id' : 'nonce' } ${nonce}` ); } await params.paymentValidationHandler(p.paymentData.txId, requestData); } } /** * @inheritdoc IPaymentHandler#createPaymentRailsV1 */ async createPaymentRailsV1( sender: DID, receiver: DID, agent: string, signer: Signer, payments: PaymentRailsInfo[], createOptions?: CreatePaymentRailsV1Options ): Promise<PaymentRequestMessage> { const paymentRequestInfo: PaymentRequestInfo[] = []; for (let i = 0; i < payments.length; i++) { const { credentials, description } = payments[i]; const dataArr: ( | Iden3PaymentRailsRequestV1 | Iden3PaymentRailsERC20RequestV1 | Iden3PaymentRailsSolanaRequestV1 | Iden3PaymentRailsSolanaSPLRequestV1 )[] = []; for (let j = 0; j < payments[i].options.length; j++) { const { nonce, amount, chainId, optionId, expirationDate } = payments[i].options[j]; const multiChainConfig = this._params.multiChainPaymentConfig?.find( (c) => c.chainId === chainId ); if (!multiChainConfig) { throw new Error(`failed request. no config for chain ${chainId}`); } const { recipient, paymentRails, options } = multiChainConfig; const option = options.find((t) => t.id === optionId); if (!option) { throw new Error(`failed request. no option for id ${optionId}`); } if ( (option.type === PaymentRequestDataType.Iden3PaymentRailsERC20RequestV1 || option.type === PaymentRequestDataType.Iden3PaymentRailsSolanaSPLRequestV1) && !option.contractAddress ) { throw new Error(`failed request. no token address for option id ${optionId}`); } const expirationDateRequired = expirationDate ?? new Date(new Date().setHours(new Date().getHours() + 1)); if ( option.type === PaymentRequestDataType.Iden3PaymentRailsSolanaRequestV1 || option.type === PaymentRequestDataType.Iden3PaymentRailsSolanaSPLRequestV1 ) { if (!createOptions?.solSigner) { throw new Error( `please provide solana signer in context for ${option.type} payment type` ); } const payment = await buildSolanaPayment( createOptions.solSigner, option, chainId, paymentRails, recipient, BigInt(amount), expirationDateRequired, nonce ); dataArr.push(payment); } else { const payment = await buildEvmPayment( signer, option, chainId, paymentRails, recipient, BigInt(amount), expirationDateRequired, nonce ); dataArr.push(payment); } } paymentRequestInfo.push({ data: dataArr, credentials, description }); } return createPaymentRequest(sender, receiver, agent, paymentRequestInfo); } private async packMessage( message: BasicMessage, senderDID: DID, mediaType: MediaType, packerParams?: PackerParams ): Promise<Uint8Array> { const responseEncoded = byteEncoder.encode(JSON.stringify(message)); const packerOpts = mediaType === MediaType.SignedMessage ? packerParams : { provingMethodAlg: packerParams?.provingMethodAlg || defaultProvingMethodAlg }; return await this._packerMgr.pack(mediaType, responseEncoded, { senderDID, ...packerOpts }); } private async handleIden3PaymentRequestCryptoV1( data: Iden3PaymentRequestCryptoV1, paymentHandler: (data: Iden3PaymentRequestCryptoV1) => Promise<string> ): Promise<Iden3PaymentCryptoV1> { if (data.expiration && new Date(data.expiration) < new Date()) { throw new Error(`failed request. expired request`); } const txId = await paymentHandler(data); return { id: data.id, '@context': 'https://schema.iden3.io/core/jsonld/payment.jsonld#Iden3PaymentCryptoV1', type: PaymentType.Iden3PaymentCryptoV1, paymentData: { txId } }; } private async handleIden3PaymentRailsRequestV1( data: Iden3PaymentRailsRequestV1, paymentHandler: (data: Iden3PaymentRailsRequestV1) => Promise<string> ): Promise<Iden3PaymentRailsV1> { if (data.expirationDate && new Date(data.expirationDate) < new Date()) { throw new Error(`failed request. expired request`); } const signer = await verifyEIP712TypedData(data, this._params.documentResolver); if (this._params.allowedSigners && !this._params.allowedSigners.includes(signer)) { throw new Error(`failed request. signer is not in the allowed signers list`); } const txId = await paymentHandler(data); const proof = Array.isArray(data.proof) ? data.proof[0] : data.proof; return { nonce: data.nonce, type: PaymentType.Iden3PaymentRailsV1, '@context': 'https://schema.iden3.io/core/jsonld/payment.jsonld#Iden3PaymentRailsV1', paymentData: { txId, chainId: proof.eip712.domain.chainId } }; } private async handleIden3PaymentRailsSolanaRequestV1( data: Iden3PaymentRailsSolanaRequestV1, paymentHandler: (data: Iden3PaymentRailsSolanaRequestV1) => Promise<string> ): Promise<Iden3PaymentRailsSolanaV1> { if (data.expirationDate && new Date(data.expirationDate) < new Date()) { throw new Error(`failed request. expired request`); } const isValid = await verifyIden3SolanaPaymentRequest(data, this._params.documentResolver); if (!isValid) { throw new Error(`failed request. invalid Solana payment request signature`); } const proof = Array.isArray(data.proof) ? data.proof[0] : data.proof; const signer = proof.verificationMethod.split(':').slice(-1)[0]; if (this._params.allowedSigners && !this._params.allowedSigners.includes(signer)) { throw new Error(`failed request. signer is not in the allowed signers list`); } const txId = await paymentHandler(data); return { nonce: data.nonce, type: PaymentType.Iden3PaymentRailsSolanaV1, '@context': 'https://schema.iden3.io/core/jsonld/payment.jsonld#Iden3PaymentRailsSolanaV1', paymentData: { txId, chainId: proof.domain.chainId } }; } private async handleIden3PaymentRailsSolanaSPLRequestV1( data: Iden3PaymentRailsSolanaSPLRequestV1, paymentHandler: (data: Iden3PaymentRailsSolanaSPLRequestV1) => Promise<string> ): Promise<Iden3PaymentRailsSolanaSPLV1> { if (data.expirationDate && new Date(data.expirationDate) < new Date()) { throw new Error(`failed request. expired request`); } const isValid = await verifyIden3SolanaPaymentRequest(data, this._params.documentResolver); if (!isValid) { throw new Error(`failed request. invalid Solana payment request signature`); } const proof = Array.isArray(data.proof) ? data.proof[0] : data.proof; const signer = proof.verificationMethod.split(':').slice(-1)[0]; if (this._params.allowedSigners && !this._params.allowedSigners.includes(signer)) { throw new Error(`failed request. signer is not in the allowed signers list`); } const txId = await paymentHandler(data); return { nonce: data.nonce, type: PaymentType.Iden3PaymentRailsSolanaSPLV1, '@context': 'https://schema.iden3.io/core/jsonld/payment.jsonld#Iden3PaymentRailsSolanaSPLV1', paymentData: { txId, chainId: proof.domain.chainId, tokenAddress: data.tokenAddress } }; } private async handleIden3PaymentRailsERC20RequestV1( data: Iden3PaymentRailsERC20RequestV1, paymentHandler: (data: Iden3PaymentRailsERC20RequestV1) => Promise<string>, approveHandler?: (data: Iden3PaymentRailsERC20RequestV1) => Promise<string> ): Promise<Iden3PaymentRailsERC20V1> { if (data.expirationDate && new Date(data.expirationDate) < new Date()) { throw new Error(`failed request. expired request`); } const signer = await verifyEIP712TypedData(data, this._params.documentResolver); if (this._params.allowedSigners && !this._params.allowedSigners.includes(signer)) { throw new Error(`failed request. signer is not in the allowed signers list`); } if (!data.features?.includes(PaymentFeatures.EIP_2612) && !approveHandler) { throw new Error(`please provide erc20TokenApproveHandler in context for ERC-20 payment type`); } if (approveHandler) { await approveHandler(data); } const txId = await paymentHandler(data); const proof = Array.isArray(data.proof) ? data.proof[0] : data.proof; return { nonce: data.nonce, type: PaymentType.Iden3PaymentRailsERC20V1, '@context': 'https://schema.iden3.io/core/jsonld/payment.jsonld#Iden3PaymentRailsERC20V1', paymentData: { txId, chainId: proof.eip712.domain.chainId, tokenAddress: data.tokenAddress } }; } }