Skip to content

Commit

Permalink
feat: superfluid one off payment processor (#968)
Browse files Browse the repository at this point in the history
  • Loading branch information
leoslr committed Oct 25, 2022
1 parent 8d5c91c commit 3893007
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 35 deletions.
1 change: 1 addition & 0 deletions packages/payment-processor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"test:watch": "yarn test --watch"
},
"dependencies": {
"@openzeppelin/contracts": "4.7.3",
"@requestnetwork/currency": "0.8.0",
"@requestnetwork/payment-detection": "0.35.0",
"@requestnetwork/smart-contracts": "0.28.0",
Expand Down
77 changes: 58 additions & 19 deletions packages/payment-processor/src/payment/erc20-escrow-payment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,31 @@ import {
} from './utils';
import { ITransactionOverrides } from './transaction-overrides';
import { encodeApproveAnyErc20 } from './erc20';
import { IPreparedTransaction } from './prepared-transaction';

/**
* Prepare the approval transaction of the payment ERC20 to be spent by the escrow contract
* @param request request to pay
* @param paymentTokenAddress currency to approve
* @param signerOrProvider the web3 provider
* @param overrides optionally overrides default transaction values, like gas
* @returns the prepared transaction
*/
export function prepareErc20EscrowApproval(
request: ClientTypes.IRequestData,
paymentTokenAddress: string,
signerOrProvider: providers.Provider | Signer = getProvider(),
overrides?: ITransactionOverrides,
): IPreparedTransaction {
const contractAddress = erc20EscrowToPayArtifact.getAddress(request.currencyInfo.network!);
const encodedTx = encodeApproveAnyErc20(paymentTokenAddress, contractAddress, signerOrProvider);
return {
data: encodedTx,
to: paymentTokenAddress,
value: 0,
...overrides,
};
}

/**
* Processes the approval transaction of the payment ERC20 to be spent by the erc20EscrowToPay
Expand All @@ -27,16 +52,14 @@ export async function approveErc20ForEscrow(
signerOrProvider: providers.Provider | Signer = getProvider(),
overrides?: ITransactionOverrides,
): Promise<ContractTransaction> {
const contractAddress = erc20EscrowToPayArtifact.getAddress(request.currencyInfo.network!);
const encodedTx = encodeApproveAnyErc20(paymentTokenAddress, contractAddress, signerOrProvider);
const preparedTx = prepareErc20EscrowApproval(
request,
paymentTokenAddress,
signerOrProvider,
overrides,
);
const signer = getSigner(signerOrProvider);
const tx = await signer.sendTransaction({
data: encodedTx,
to: paymentTokenAddress,
value: 0,
...overrides,
});
return tx;
return await signer.sendTransaction(preparedTx);
}

/**
Expand All @@ -54,17 +77,9 @@ export async function payEscrow(
feeAmount?: BigNumberish,
overrides?: ITransactionOverrides,
): Promise<ContractTransaction> {
const encodedTx = encodePayEscrow(request, amount, feeAmount);
const contractAddress = erc20EscrowToPayArtifact.getAddress(request.currencyInfo.network!);
const preparedTx = preparePayEscrow(request, amount, feeAmount, overrides);
const signer = getSigner(signerOrProvider);

const tx = await signer.sendTransaction({
data: encodedTx,
to: contractAddress,
value: 0,
...overrides,
});
return tx;
return await signer.sendTransaction(preparedTx);
}

/**
Expand Down Expand Up @@ -244,6 +259,30 @@ export function encodePayEscrow(
]);
}

/**
* Prepare a transaction pay the escrow contract.
* @param request request to pay.
* @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum.
* @param amount optional, if you want to override the amount in the request.
* @param feeAmount optional, if you want to override the feeAmount in the request.
* @param overrides optionally, override default transaction values, like gas.
*/
export function preparePayEscrow(
request: ClientTypes.IRequestData,
amount?: BigNumberish,
feeAmount?: BigNumberish,
overrides?: ITransactionOverrides,
): IPreparedTransaction {
const encodedTx = encodePayEscrow(request, amount, feeAmount);
const contractAddress = erc20EscrowToPayArtifact.getAddress(request.currencyInfo.network!);
return {
data: encodedTx,
to: contractAddress,
value: 0,
...overrides,
};
}

/**
* Encapsulates the validation, paymentReference calculation and escrow contract interface creation.
* These steps are used in all subsequent functions encoding escrow interaction transactions
Expand Down
66 changes: 65 additions & 1 deletion packages/payment-processor/src/payment/erc777-stream.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { ContractTransaction, Signer, Overrides, providers, BigNumberish } from 'ethers';
import {
ContractTransaction,
Signer,
Overrides,
providers,
BigNumberish,
BigNumber,
ethers,
} from 'ethers';

import { ClientTypes, ExtensionTypes, PaymentTypes } from '@requestnetwork/types';
import { getPaymentNetworkExtension } from '@requestnetwork/payment-detection';

import { getNetworkProvider, getProvider, getRequestPaymentValues, validateRequest } from './utils';
import { Framework } from '@superfluid-finance/sdk-core';
import { IPreparedTransaction } from './prepared-transaction';
import { ITransactionOverrides } from './transaction-overrides';
import * as erc777Artefact from '@openzeppelin/contracts/build/contracts/IERC777.json';

export const RESOLVER_ADDRESS = '0x913bbCFea2f347a24cfCA441d483E7CBAc8De3Db';
// Superfluid payments of requests use the generic field `userData` to index payments.
Expand Down Expand Up @@ -188,3 +198,57 @@ export async function getErc777BalanceAt(
});
return realtimeBalance.availableBalance;
}

/**
* Encode the transaction data for a one off payment of ERC777 Tokens
* @param request to encode the payment for
* @param amount the amount to be sent
* @returns the encoded transaction data
*/
export const encodeErc777OneOffPayment = (
request: ClientTypes.IRequestData,
amount: BigNumber,
): string => {
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 { paymentReference, paymentAddress } = getRequestPaymentValues(request);
const erc777 = ethers.ContractFactory.getInterface(erc777Artefact.abi);
return erc777.encodeFunctionData('send', [paymentAddress, amount, `0x${paymentReference}`]);
};

/**
* Prepare the transaction for a one payment for the user to sign
* @param request to prepare the transaction for
* @param amount the amount to be sent
* @returns the prepared transaction
*/
export const prepareErc777OneOffPayment = (
request: ClientTypes.IRequestData,
amount: BigNumber,
): IPreparedTransaction => {
return {
data: encodeErc777OneOffPayment(request, amount),
to: request.currencyInfo.value,
value: 0,
};
};

/**
* Make an ERC777 payment
* @param request associated to the payment
* @param amount the amount to be sent
* @param signer the transaction signer
* @returns the transaction result
*/
export const makeErc777OneOffPayment = async (
request: ClientTypes.IRequestData,
amount: BigNumber,
signer: Signer,
overrides?: ITransactionOverrides,
): Promise<ContractTransaction> => {
const preparedTx = prepareErc777OneOffPayment(request, amount);
return signer.sendTransaction({ ...preparedTx, ...overrides });
};
68 changes: 53 additions & 15 deletions packages/payment-processor/src/payment/erc777-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,22 @@ export async function approveUnderlyingToken(
return signer.sendTransaction(preparedTx);
}

/**
* Prepare the wrap transaction of the specified amount of underlying token into supertoken
* @param request the request that contains currency information
* @param provider the web3 provider
* @param amount to allow, defaults to max allowance
* @returns
*/
export async function prepareWrapUnderlyingToken(
request: ClientTypes.IRequestData,
provider: providers.Provider = getNetworkProvider(request),
amount: BigNumber = MAX_ALLOWANCE,
): Promise<IPreparedTransaction> {
const wrapOp = await getWrapUnderlyingTokenOp(request, provider, amount);
return (await wrapOp.populateTransactionPromise) as IPreparedTransaction;
}

/**
* Wrap the speicified amount of underlying token into supertokens
* @param request the request that contains currency information
Expand All @@ -176,8 +192,37 @@ export async function wrapUnderlyingToken(
if (!(await hasEnoughUnderlyingToken(request, senderAddress, provider, amount))) {
throw new Error('Sender does not have enough underlying token');
}
const wrapOp = await getWrapUnderlyingTokenOp(request, signer.provider ?? getProvider(), amount);
return wrapOp.exec(signer);
const preparedTx = await prepareWrapUnderlyingToken(
request,
signer.provider ?? getProvider(),
amount,
);
return signer.sendTransaction(preparedTx);
}

/**
* Prepare the unwrapping transaction of the supertoken (ERC777) into underlying asset (ERC20)
* @param request the request that contains currency information
* @param provider the web3 provider
* @param amount to unwrap
*/
export async function prepareUnwrapSuperToken(
request: ClientTypes.IRequestData,
provider: providers.Provider = getNetworkProvider(request),
amount: BigNumber,
): Promise<IPreparedTransaction> {
const sf = await getSuperFluidFramework(request, provider);
const superToken = await sf.loadSuperToken(request.currencyInfo.value);
const underlyingToken = await getRequestUnderlyingToken(request, provider);

if (underlyingToken.address === superToken.address) {
throw new Error('This is a native super token');
}

const downgradeOp = superToken.downgrade({
amount: amount.toString(),
});
return (await downgradeOp.populateTransactionPromise) as IPreparedTransaction;
}

/**
Expand All @@ -193,15 +238,6 @@ export async function unwrapSuperToken(
): Promise<ContractTransaction> {
const sf = await getSuperFluidFramework(request, signer.provider ?? getProvider());
const superToken = await sf.loadSuperToken(request.currencyInfo.value);
const underlyingToken = await getRequestUnderlyingToken(
request,
signer.provider ?? getProvider(),
);

if (underlyingToken.address === superToken.address) {
throw new Error('This is a native super token');
}

const userAddress = await signer.getAddress();
const userBalance = await superToken.balanceOf({
account: userAddress,
Expand All @@ -210,8 +246,10 @@ export async function unwrapSuperToken(
if (amount.gt(userBalance)) {
throw new Error('Sender does not have enough supertoken');
}
const downgradeOp = superToken.downgrade({
amount: amount.toString(),
});
return downgradeOp.exec(signer);
const preparedTx = await prepareUnwrapSuperToken(
request,
signer.provider ?? getProvider(),
amount,
);
return signer.sendTransaction(preparedTx);
}
51 changes: 51 additions & 0 deletions packages/payment-processor/test/payment/erc777-stream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Utils from '@requestnetwork/utils';

import {
completeErc777StreamRequest,
makeErc777OneOffPayment,
payErc777StreamRequest,
RESOLVER_ADDRESS,
} from '../../src/payment/erc777-stream';
Expand All @@ -29,6 +30,7 @@ const expectedFlowRate = '100000';
const expectedStartDate = '1643041225';
const provider = new providers.JsonRpcProvider('http://localhost:8545');
const wallet = Wallet.fromMnemonic(mnemonic).connect(provider);
const oneOffPaymentAmount = '1000000000';

const validRequest: ClientTypes.IRequestData = {
balance: {
Expand Down Expand Up @@ -239,4 +241,53 @@ describe('erc777-stream', () => {
expect(paymentFlowRate).toBe('0');
});
});

describe('makeErc777OneOffPayment', () => {
it('Should perform a payment', async () => {
// 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');

const senderBalanceBefore = await daix.balanceOf({
account: wallet.address,
providerOrSigner: provider,
});
const recipientBalanceBefore = await daix.balanceOf({
account: paymentAddress,
providerOrSigner: provider,
});

// Perform the payment
const tx = await makeErc777OneOffPayment(
validRequest,
BigNumber.from(oneOffPaymentAmount),
wallet,
);
await tx.wait(1);

const senderBalanceAfter = await daix.balanceOf({
account: wallet.address,
providerOrSigner: provider,
});
const recipientBalanceAfter = await daix.balanceOf({
account: paymentAddress,
providerOrSigner: provider,
});

expect(BigNumber.from(senderBalanceBefore)).toEqual(
BigNumber.from(senderBalanceAfter).add(oneOffPaymentAmount),
);
expect(BigNumber.from(recipientBalanceAfter)).toEqual(
BigNumber.from(recipientBalanceBefore).add(oneOffPaymentAmount),
);
});
});
});
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5131,6 +5131,11 @@
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.5.0.tgz#3fd75d57de172b3743cdfc1206883f56430409cc"
integrity sha512-fdkzKPYMjrRiPK6K4y64e6GzULR7R7RwxSigHS8DDp7aWDeoReqsQI+cxHV1UuhAqX69L1lAaWDxenfP+xiqzA==

"@openzeppelin/contracts@4.7.3":
version "4.7.3"
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.7.3.tgz#939534757a81f8d69cc854c7692805684ff3111e"
integrity sha512-dGRS0agJzu8ybo44pCIf3xBaPQN/65AIXNgK8+4gzKd5kbvlqyxryUYVLJv7fK98Seyd2hDZzVEHSWAh0Bt1Yw==

"@openzeppelin/test-helpers@0.5.6":
version "0.5.6"
resolved "https://registry.yarnpkg.com/@openzeppelin/test-helpers/-/test-helpers-0.5.6.tgz#cafa3fdb741be9e3ff525916257d6cdce45ed86a"
Expand Down

0 comments on commit 3893007

Please sign in to comment.