import axios, { AxiosResponse } from "axios"; import BigNumber from "bignumber.js"; import { Token, TokenAmount } from "../Tokens"; import { SerializedTokenAmount } from "../Tokens/TokenAmount"; import { BigNumberString, ChainId, WALLET_SERVICE_URL } from "../constants"; import { Wallet } from "../wallet"; export interface PaymentRequestData { id: number; payer: string; payee: string; amount: TokenAmount; totalAmount: TokenAmount; } export interface PaymentRequestPayload { rid: number; payer: string; payee: string; token: string; total: string; amount: string; deadline?: string; metadata?: Record; requested_on: string; chain: ChainId; } export interface PaymentRequestDto { payee: string; payer: string; token: string; total: string; deadline?: number; metadata?: Record; } export interface SerializedPaymentRequest { _type: "PaymentRequest"; rid: number; payer: string; payee: string; amount: SerializedTokenAmount; totalAmount: SerializedTokenAmount; metadata: Record; requestedOn: string; fulfilledOn: string; deadline: string; } /** * Class representation of a Payment request. Facilitates queries and fulfillment */ export class PaymentRequest< MetaDataShape extends Record = Record > { private onFulfill?: (req?: PaymentRequest) => unknown; constructor( private id: number, private payer: string, private payee: string, private amount: TokenAmount, private totalAmount: TokenAmount, private _metadata?: MetaDataShape, private _requestedOn?: Date, private _fulfilledOn?: Date, private _deadline?: Date ) {} /** * Basic info on the given payment request */ get info(): PaymentRequestData { return { id: this.id, payer: this.payer, payee: this.payee, amount: this.amount, totalAmount: this.totalAmount, }; } /** * Custom metadata as defined in the request origination. Shape influences Payment request. */ get metadata() { return this._metadata; } get deadline() { return this._deadline; } get requestedOn() { return this._requestedOn; } get fulfilledOn() { return this._fulfilledOn; } /** * * @param field Field to access * @returns corresponding value in the metadata */ get(field: keyof MetaDataShape) { return this._metadata?.[field]; } setOnFulfillCallback(f: (req?: PaymentRequest) => unknown) { this.onFulfill = f; } /** * * @param transactionHash Hash where payment was sen * @returns Receipt of payment being counted in the wallet service */ async fulfill(transactionHash: string): Promise<{ request: PaymentRequest; receipt: { amountCredited: TokenAmount; isFullyRepaid: boolean; hash: string; rid: number; time: number; }; }> { const resp = await axios.post< { hash: string }, AxiosResponse<{ amountCredited: BigNumberString; isFullyRepaid: boolean; hash: string; rid: number; time: number; }> >(`${WALLET_SERVICE_URL}payments/requests/${this.id}/fulfill`, { hash: transactionHash, }); const remaining = this.amount.raw.minus(resp.data.amountCredited); if (this.onFulfill) this.onFulfill(); return { request: new PaymentRequest( this.id, this.payer, this.payee, new TokenAmount(this.amount.token, remaining), this.totalAmount, this._metadata, this._requestedOn, resp.data.isFullyRepaid ? new Date(resp.data.time) : this._fulfilledOn, this._deadline ), receipt: { ...resp.data, amountCredited: new TokenAmount( this.amount.token, new BigNumber(resp.data.amountCredited) ), }, }; } public static isSerializedPaymentRequest = ( obj?: any ): obj is SerializedPaymentRequest => obj?._type === "PaymentRequest"; public static parseJson = ({ rid, payer, payee, amount, totalAmount, deadline, metadata, fulfilledOn, requestedOn, }: SerializedPaymentRequest) => new PaymentRequest( rid, payer, payee, TokenAmount.parseJson(amount), TokenAmount.parseJson(totalAmount), metadata, new Date(requestedOn), new Date(fulfilledOn), new Date(deadline) ); toJSON = () => ({ _type: "PaymentRequest", rid: this.id, payer: this.payer, payee: this.payee, amount: JSON.stringify(this.amount), totalAmount: JSON.stringify(this.totalAmount), metadata: JSON.stringify(this.metadata), requestedOn: JSON.stringify(this.requestedOn), fulfilledOn: JSON.stringify(this.fulfilledOn), deadline: JSON.stringify(this.deadline), }); /** * * @param json JSON-representation to serialize * @param getToken method to fetch a token given a token address * @returns PaymenRequest object populated with fields from json */ static deserialize< MetaDataShape extends Record = Record >(json: PaymentRequestPayload, getToken: (address: string) => Token) { const { rid, payee, payer, token: tokenAddress, amount, deadline, metadata, requested_on, total, } = json; const token = getToken(tokenAddress); return new PaymentRequest( rid, payer, payee, new TokenAmount(token, new BigNumber(amount)), new TokenAmount(token, new BigNumber(total)), metadata as MetaDataShape, new Date(requested_on), undefined, deadline ? new Date(deadline) : undefined ); } /** * * @param id ID of payment request to fetch * @param getToken Helper method to fetch a token given its address * @param apiKey Optional api key * @returns PaymentRequest object of corresponding ID */ static async fetch< MetaDataShape extends Record = Record >(id: number, getToken: (adress: string) => Token, apiKey?: string) { const info = await axios.get( `${WALLET_SERVICE_URL}payments/requests/${id}`, { headers: { "X-API-KEY": apiKey ?? "", }, } ); return PaymentRequest.deserialize(info.data, getToken); } /** * * @param payee Address of wallet where payments should be sent * @param payer Address of wallet where payments should originate * @param amount Token and amount to be request * @param signer Wallet object to authorize the request * @param opts optionally declare a deadline for the request, or set metadata for the request * @param apiKey Optional api key for Node Finance * @returns PaymentRequest object of newly-created request */ static async create< MetaDataShape extends Record = Record >( payee: string, payer: string, amount: TokenAmount, signer: Wallet, opts?: { deadline?: Date | number; metadata?: MetaDataShape; }, apiKey?: string ): Promise> { const body: PaymentRequestDto = { payee, payer, token: amount.token.address, total: amount.raw.toFixed(0), metadata: opts?.metadata, }; if (opts?.deadline) { body.deadline = typeof opts.deadline === "number" ? opts.deadline : opts.deadline.valueOf(); } const sig = signer.signMessage(JSON.stringify(body)); const { data: resp } = await axios.post< PaymentRequestDto, AxiosResponse >(`${WALLET_SERVICE_URL}payments/requests`, body, { headers: { "X-API-KEY": apiKey ?? "", "X-SIGNATURE": sig.signature, }, }); return new PaymentRequest( resp.rid, resp.payer, resp.payee, amount, amount, resp.metadata as MetaDataShape, resp.deadline ? new Date(resp.deadline) : undefined ); } }