From 56afdcffb426bcd3cc386ee4bb95f301d41336ff Mon Sep 17 00:00:00 2001 From: Bertrand Juglas Date: Tue, 12 Jul 2022 19:04:45 +0200 Subject: [PATCH] feat: add cancel stream function (#884) --- .../src/payment/erc777-stream.ts | 99 ++++++++++++++----- .../test/payment/erc777-stream.test.ts | 58 +++++++++-- 2 files changed, 126 insertions(+), 31 deletions(-) diff --git a/packages/payment-processor/src/payment/erc777-stream.ts b/packages/payment-processor/src/payment/erc777-stream.ts index 01a627db93..ef25531180 100644 --- a/packages/payment-processor/src/payment/erc777-stream.ts +++ b/packages/payment-processor/src/payment/erc777-stream.ts @@ -10,12 +10,16 @@ import { } from './utils'; import { Framework } from '@superfluid-finance/sdk-core'; -export const resolverAddress = '0x913bbCFea2f347a24cfCA441d483E7CBAc8De3Db'; +export const RESOLVER_ADDRESS = '0x913bbCFea2f347a24cfCA441d483E7CBAc8De3Db'; +// Superfluid payments of requests use the generic field `userData` to index payments. +// Since it's a multi-purpose field, payments will use a fix-prefix heading the payment reference, +// in order to speed up the indexing and payment detection. +export const USERDATA_PREFIX = '0xbeefac'; /** * Processes a transaction to pay an ERC777 stream Request. * @param request - * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. + * @param signer the Web3 signer. Defaults to window.ethereum. * @param overrides optionally, override default transaction values, like gas. */ export async function payErc777StreamRequest( @@ -28,35 +32,84 @@ export async function payErc777StreamRequest( throw new Error('Not a supported ERC777 payment network request'); } validateRequest(request, PaymentTypes.PAYMENT_NETWORK_ID.ERC777_STREAM); - const networkName = - request.currencyInfo.network === 'private' ? 'custom' : request.currencyInfo.network; - const sf = await Framework.create({ - networkName, - provider: signer.provider ?? getProvider(), - dataMode: request.currencyInfo.network === 'private' ? 'WEB3_ONLY' : undefined, - resolverAddress: request.currencyInfo.network === 'private' ? resolverAddress : undefined, - protocolReleaseVersion: request.currencyInfo.network === 'private' ? 'test' : undefined, - }); - const superSigner = sf.createSigner({ - signer: signer, - provider: signer.provider, - }); - const superToken = await sf.loadSuperToken(request.currencyInfo.value); + const sf = await getSuperFluidFramework(request, signer); // FIXME: according to specs PR https://github.com/RequestNetwork/requestNetwork/pull/688 // in file packages/advanced-logic/specs/payment-network-erc777-stream-0.1.0.md + // Below are the SF actions to add in the BatchCall: // - use expectedStartDate to compute offset between start of invoicing and start of streaming // - start fee streaming + const streamPayOp = await getStartStreamOp(sf, request, overrides); + const batchCall = sf.batchCall([streamPayOp]); + return batchCall.exec(signer); +} + +/** + * Processes a transaction to complete an ERC777 stream paying a Request. + * @param request + * @param signer the Web3 signer. Defaults to window.ethereum. + * @param overrides optionally, override default transaction values, like gas. + */ +export async function completeErc777StreamRequest( + request: ClientTypes.IRequestData, + signer: Signer, + overrides?: Overrides, +): Promise { + const id = getPaymentNetworkExtension(request)?.id; + if (id !== ExtensionTypes.ID.PAYMENT_NETWORK_ERC777_STREAM) { + throw new Error('Not a supported ERC777 payment network request'); + } + validateRequest(request, PaymentTypes.PAYMENT_NETWORK_ID.ERC777_STREAM); + const sf = await getSuperFluidFramework(request, signer); + // FIXME: according to specs PR https://github.com/RequestNetwork/requestNetwork/pull/688 + // in file packages/advanced-logic/specs/payment-network-erc777-stream-0.1.0.md + // Below are the SF actions to add in the BatchCall : + // - use expectedEndDate to compute offset between stop of invoicing and stop of streaming + // - stop fee streaming + const streamPayOp = await getStopStreamOp(sf, signer, request, overrides); + const batchCall = sf.batchCall([streamPayOp]); + return batchCall.exec(signer); +} + +async function getSuperFluidFramework(request: ClientTypes.IRequestData, signer: Signer) { + const isNetworkPrivate = request.currencyInfo.network === 'private'; + const networkName = isNetworkPrivate ? 'custom' : request.currencyInfo.network; + return await Framework.create({ + networkName, + provider: signer.provider ?? getProvider(), + dataMode: isNetworkPrivate ? 'WEB3_ONLY' : undefined, + resolverAddress: isNetworkPrivate ? RESOLVER_ADDRESS : undefined, + protocolReleaseVersion: isNetworkPrivate ? 'test' : undefined, + }); +} +async function getStartStreamOp( + sf: Framework, + request: ClientTypes.IRequestData, + overrides?: Overrides, +) { + const superToken = await sf.loadSuperToken(request.currencyInfo.value); const { paymentReference, paymentAddress, expectedFlowRate } = getRequestPaymentValues(request); - // Superfluid payments of requests use the generic field `userData` to index payments. - // Since it's a multi-purpose field, payments will use a fix-prefix heading the payment reference, - // in order to speed up the indexing and payment detection. - const streamPayOp = sf.cfaV1.createFlow({ + return sf.cfaV1.createFlow({ flowRate: expectedFlowRate ?? '0', receiver: paymentAddress, superToken: superToken.address, - userData: `0xbeefac${paymentReference}`, + userData: `${USERDATA_PREFIX}${paymentReference}`, + overrides: overrides, + }); +} + +async function getStopStreamOp( + sf: Framework, + signer: Signer, + request: ClientTypes.IRequestData, + overrides?: Overrides, +) { + const superToken = await sf.loadSuperToken(request.currencyInfo.value); + const { paymentReference, paymentAddress } = getRequestPaymentValues(request); + return sf.cfaV1.deleteFlow({ + superToken: superToken.address, + sender: await signer.getAddress(), + receiver: paymentAddress, + userData: `${USERDATA_PREFIX}${paymentReference}`, overrides: overrides, }); - const batchCall = sf.batchCall([streamPayOp]); - return batchCall.exec(superSigner); } diff --git a/packages/payment-processor/test/payment/erc777-stream.test.ts b/packages/payment-processor/test/payment/erc777-stream.test.ts index 605e3f03d8..644e71225f 100644 --- a/packages/payment-processor/test/payment/erc777-stream.test.ts +++ b/packages/payment-processor/test/payment/erc777-stream.test.ts @@ -10,7 +10,11 @@ import { } from '@requestnetwork/types'; import Utils from '@requestnetwork/utils'; -import { payErc777StreamRequest, resolverAddress } from '../../src/payment/erc777-stream'; +import { + completeErc777StreamRequest, + payErc777StreamRequest, + RESOLVER_ADDRESS, +} from '../../src/payment/erc777-stream'; import { getRequestPaymentValues } from '../../src/payment/utils'; const daiABI = require('../abis/fDAIABI'); @@ -120,8 +124,8 @@ describe('erc777-stream', () => { }); }); - describe('payErc777StreamRequest', () => { - it('should pay an ERC777 request with fees', async () => { + describe('Streams management', () => { + it('payErc777StreamRequest should pay an ERC777 request', async () => { let tx; let confirmedTx; // initialize the superfluid framework...put custom and web3 only bc we are using ganache locally @@ -129,7 +133,7 @@ describe('erc777-stream', () => { networkName: 'custom', provider, dataMode: 'WEB3_ONLY', - resolverAddress: resolverAddress, + resolverAddress: RESOLVER_ADDRESS, protocolReleaseVersion: 'test', }); @@ -183,18 +187,56 @@ describe('erc777-stream', () => { expect(confirmedTx.status).toBe(1); expect(tx.hash).not.toBeUndefined(); - const wFlowRate = await sf.cfaV1.getNetFlow({ + const walletFlowRate = await sf.cfaV1.getNetFlow({ + superToken: daix.address, + account: wallet.address, + providerOrSigner: provider, + }); + expect(walletFlowRate).toBe(`-${expectedFlowRate}`); + const paymentFlowRate = await sf.cfaV1.getNetFlow({ + superToken: daix.address, + account: paymentAddress, + providerOrSigner: provider, + }); + expect(paymentFlowRate).toBe(expectedFlowRate); + }); + + it('completeErc777StreamRequest should complete an ERC777 request', async () => { + let tx; + let confirmedTx; + // initialize the superfluid framework...put custom and web3 only bc we are using ganache locally + const sf = await Framework.create({ + networkName: 'custom', + provider, + dataMode: 'WEB3_ONLY', + resolverAddress: RESOLVER_ADDRESS, + protocolReleaseVersion: 'test', + }); + + // use the framework to get the SuperToken + const daix = await sf.loadSuperToken('fDAIx'); + + // wait 2 seconds of streaming to avoid failing + await new Promise((r) => setTimeout(r, 2000)); + + // Stopping fDAIX stream request + tx = await completeErc777StreamRequest(validRequest, wallet); + confirmedTx = await tx.wait(1); + expect(confirmedTx.status).toBe(1); + expect(tx.hash).not.toBeUndefined(); + + const walletFlowRate = await sf.cfaV1.getNetFlow({ superToken: daix.address, account: wallet.address, providerOrSigner: provider, }); - expect(wFlowRate).toBe(`-${expectedFlowRate}`); - const pFlowRate = await sf.cfaV1.getNetFlow({ + expect(walletFlowRate).toBe('0'); + const paymentFlowRate = await sf.cfaV1.getNetFlow({ superToken: daix.address, account: paymentAddress, providerOrSigner: provider, }); - expect(pFlowRate).toBe(expectedFlowRate); + expect(paymentFlowRate).toBe('0'); }); }); });