Skip to content

Commit e25862d

Browse files
authored
feat: erc777 helpers (#944)
1 parent a9e3f3e commit e25862d

File tree

10 files changed

+466
-19
lines changed

10 files changed

+466
-19
lines changed

packages/payment-processor/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from './payment/erc20';
44
export * from './payment/erc20-proxy';
55
export * from './payment/erc20-fee-proxy';
66
export * from './payment/erc777-stream';
7+
export * from './payment/erc777-utils';
78
export * from './payment/eth-input-data';
89
export * from './payment/near-input-data';
910
export * from './payment/near-conversion';

packages/payment-processor/src/payment/erc20.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
getProvider,
1515
getProxyAddress as genericGetProxyAddress,
1616
getSigner,
17+
MAX_ALLOWANCE,
1718
validateRequest,
1819
} from './utils';
1920
import { IPreparedTransaction } from './prepared-transaction';
@@ -107,9 +108,10 @@ export async function approveErc20IfNeeded(
107108
account: string,
108109
signerOrProvider: providers.Provider | Signer = getNetworkProvider(request),
109110
overrides?: ITransactionOverrides,
111+
amount: BigNumber = MAX_ALLOWANCE,
110112
): Promise<ContractTransaction | void> {
111113
if (!(await hasErc20Approval(request, account, signerOrProvider))) {
112-
return approveErc20(request, getSigner(signerOrProvider), overrides);
114+
return approveErc20(request, getSigner(signerOrProvider), overrides, amount);
113115
}
114116
}
115117

@@ -124,8 +126,9 @@ export async function approveErc20(
124126
request: ClientTypes.IRequestData,
125127
signerOrProvider: providers.Provider | Signer = getProvider(),
126128
overrides?: ITransactionOverrides,
129+
amount: BigNumber = MAX_ALLOWANCE,
127130
): Promise<ContractTransaction> {
128-
const preparedTx = prepareApproveErc20(request, signerOrProvider, overrides);
131+
const preparedTx = prepareApproveErc20(request, signerOrProvider, overrides, amount);
129132
const signer = getSigner(signerOrProvider);
130133
const tx = await signer.sendTransaction(preparedTx);
131134
return tx;
@@ -142,8 +145,9 @@ export function prepareApproveErc20(
142145
request: ClientTypes.IRequestData,
143146
signerOrProvider: providers.Provider | Signer = getProvider(),
144147
overrides?: ITransactionOverrides,
148+
amount: BigNumber = MAX_ALLOWANCE,
145149
): IPreparedTransaction {
146-
const encodedTx = encodeApproveErc20(request, signerOrProvider);
150+
const encodedTx = encodeApproveErc20(request, signerOrProvider, amount);
147151
const tokenAddress = request.currencyInfo.value;
148152
return {
149153
data: encodedTx,
@@ -162,6 +166,7 @@ export function prepareApproveErc20(
162166
export function encodeApproveErc20(
163167
request: ClientTypes.IRequestData,
164168
signerOrProvider: providers.Provider | Signer = getProvider(),
169+
amount: BigNumber = MAX_ALLOWANCE,
165170
): string {
166171
const paymentNetworkId = getPaymentNetworkExtension(request)
167172
?.id as unknown as PaymentTypes.PAYMENT_NETWORK_ID;
@@ -173,6 +178,7 @@ export function encodeApproveErc20(
173178
request.currencyInfo.value,
174179
getProxyAddress(request),
175180
getSigner(signerOrProvider),
181+
amount,
176182
);
177183
}
178184

@@ -181,20 +187,19 @@ export function encodeApproveErc20(
181187
* @param tokenAddress the ERC20 token address to approve
182188
* @param spenderAddress the address granted the approval
183189
* @param signerOrProvider the signer who owns ERC20 tokens
190+
* @param amount default to max allowance
184191
*/
185192
export function encodeApproveAnyErc20(
186193
tokenAddress: string,
187194
spenderAddress: string,
188195
signerOrProvider: providers.Provider | Signer = getProvider(),
196+
amount: BigNumber = MAX_ALLOWANCE,
189197
): string {
198+
if (amount.gt(MAX_ALLOWANCE)) {
199+
throw new Error('Invalid amount');
200+
}
190201
const erc20interface = ERC20__factory.connect(tokenAddress, signerOrProvider).interface;
191-
return erc20interface.encodeFunctionData('approve', [
192-
spenderAddress,
193-
BigNumber.from(2)
194-
// eslint-disable-next-line no-magic-numbers
195-
.pow(256)
196-
.sub(1),
197-
]);
202+
return erc20interface.encodeFunctionData('approve', [spenderAddress, amount]);
198203
}
199204

200205
/**

packages/payment-processor/src/payment/erc777-stream.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ export async function completeErc777StreamRequest(
6363
* @param request the request to pay
6464
* @param provider the Web3 provider. Defaults to window.ethereum.
6565
*/
66-
async function getSuperFluidFramework(
66+
export async function getSuperFluidFramework(
6767
request: ClientTypes.IRequestData,
6868
provider: providers.Provider,
69-
) {
69+
): Promise<Framework> {
7070
const isNetworkPrivate = request.currencyInfo.network === 'private';
7171
const networkName = isNetworkPrivate ? 'custom' : request.currencyInfo.network;
7272
return await Framework.create({
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import { ContractTransaction, Signer, providers, BigNumberish, BigNumber } from 'ethers';
2+
3+
import { ClientTypes, ExtensionTypes, PaymentTypes } from '@requestnetwork/types';
4+
import { getPaymentNetworkExtension } from '@requestnetwork/payment-detection';
5+
6+
import { getNetworkProvider, getProvider, validateRequest, MAX_ALLOWANCE } from './utils';
7+
import Token from '@superfluid-finance/sdk-core/dist/module/Token';
8+
import { getSuperFluidFramework } from './erc777-stream';
9+
import Operation from '@superfluid-finance/sdk-core/dist/module/Operation';
10+
import { checkErc20Allowance, encodeApproveAnyErc20, getAnyErc20Balance } from './erc20';
11+
import { IPreparedTransaction } from './prepared-transaction';
12+
13+
/**
14+
* Gets the underlying token address of an ERC777 currency based request
15+
* @param request the request that contains currency information
16+
* @param provider the web3 provider. Defaults to Etherscan
17+
*/
18+
export async function getRequestUnderlyingToken(
19+
request: ClientTypes.IRequestData,
20+
provider: providers.Provider = getNetworkProvider(request),
21+
): Promise<Token> {
22+
const id = getPaymentNetworkExtension(request)?.id;
23+
if (id !== ExtensionTypes.ID.PAYMENT_NETWORK_ERC777_STREAM) {
24+
throw new Error('Not a supported ERC777 payment network request');
25+
}
26+
validateRequest(request, PaymentTypes.PAYMENT_NETWORK_ID.ERC777_STREAM);
27+
const sf = await getSuperFluidFramework(request, provider);
28+
const superToken = await sf.loadSuperToken(request.currencyInfo.value);
29+
return superToken.underlyingToken;
30+
}
31+
32+
/**
33+
* Gets the underlying token address of an ERC777 currency based request
34+
* @param request the request that contains currency information
35+
* @param provider the web3 provider. Defaults to Etherscan
36+
*/
37+
export async function getUnderlyingTokenBalanceOf(
38+
request: ClientTypes.IRequestData,
39+
address: string,
40+
provider: providers.Provider = getNetworkProvider(request),
41+
): Promise<BigNumberish> {
42+
const underlyingToken = await getRequestUnderlyingToken(request, provider);
43+
return getAnyErc20Balance(underlyingToken.address, address, provider);
44+
}
45+
46+
/**
47+
* Check if the user has the specified amount of underlying token
48+
* @param request the request that contains currency information
49+
* @param address token owner
50+
* @param provider the web3 provider. Defaults to Etherscan
51+
* @param amount the required amount
52+
*/
53+
export async function hasEnoughUnderlyingToken(
54+
request: ClientTypes.IRequestData,
55+
address: string,
56+
provider: providers.Provider = getNetworkProvider(request),
57+
amount: BigNumber,
58+
): Promise<boolean> {
59+
const balance = await getUnderlyingTokenBalanceOf(request, address, provider);
60+
return amount.lte(balance);
61+
}
62+
63+
/**
64+
* Determine whether or not the supertoken has enough allowance
65+
* @param request the request that contains currency information
66+
* @param address token owner
67+
* @param provider the web3 provider. Defaults to Etherscan
68+
* @param amount of token required
69+
*/
70+
export async function checkSuperTokenUnderlyingAllowance(
71+
request: ClientTypes.IRequestData,
72+
address: string,
73+
provider: providers.Provider = getNetworkProvider(request),
74+
amount: BigNumber = MAX_ALLOWANCE,
75+
): Promise<boolean> {
76+
const underlyingToken = await getRequestUnderlyingToken(request, provider);
77+
return checkErc20Allowance(
78+
address,
79+
request.currencyInfo.value,
80+
provider,
81+
underlyingToken.address,
82+
amount,
83+
);
84+
}
85+
86+
/**
87+
* Get the SF operation to approve the supertoken to spend underlying tokens
88+
* @param request the request that contains currency information
89+
* @param provider the web3 provider. Defaults to Etherscan
90+
* @param amount to allow, defalts to max allowance
91+
*/
92+
export async function prepareApproveUnderlyingToken(
93+
request: ClientTypes.IRequestData,
94+
provider: providers.Provider = getNetworkProvider(request),
95+
amount: BigNumber = MAX_ALLOWANCE,
96+
): Promise<IPreparedTransaction> {
97+
const underlyingToken = await getRequestUnderlyingToken(request, provider);
98+
return {
99+
data: encodeApproveAnyErc20(
100+
underlyingToken.address,
101+
request.currencyInfo.value,
102+
provider,
103+
amount,
104+
),
105+
to: underlyingToken.address,
106+
value: 0,
107+
};
108+
}
109+
110+
/**
111+
* Get the SF operation to Wrap the underlying asset into supertoken
112+
* @param request the request that contains currency information
113+
* @param address the user address
114+
* @param provider the web3 provider
115+
* @param amount to allow, defalts to max allowance
116+
*/
117+
export async function getWrapUnderlyingTokenOp(
118+
request: ClientTypes.IRequestData,
119+
provider: providers.Provider = getNetworkProvider(request),
120+
amount: BigNumber,
121+
): Promise<Operation> {
122+
const sf = await getSuperFluidFramework(request, provider);
123+
const superToken = await sf.loadSuperToken(request.currencyInfo.value);
124+
return superToken.upgrade({
125+
amount: amount.toString(),
126+
});
127+
}
128+
129+
/**
130+
* Approve the supertoken to spend the speicified amount of underlying token
131+
* @param request the request that contains currency information
132+
* @param signer the web3 signer
133+
* @param amount to allow, defaults to max allowance
134+
* @returns
135+
*/
136+
export async function approveUnderlyingToken(
137+
request: ClientTypes.IRequestData,
138+
signer: Signer,
139+
amount: BigNumber = MAX_ALLOWANCE,
140+
): Promise<ContractTransaction> {
141+
if (
142+
!(await hasEnoughUnderlyingToken(
143+
request,
144+
await signer.getAddress(),
145+
signer.provider ?? getProvider(),
146+
amount,
147+
))
148+
) {
149+
throw new Error('Sender does not have enough underlying token');
150+
}
151+
const preparedTx = await prepareApproveUnderlyingToken(
152+
request,
153+
signer.provider ?? getProvider(),
154+
amount,
155+
);
156+
return signer.sendTransaction(preparedTx);
157+
}
158+
159+
/**
160+
* Wrap the speicified amount of underlying token into supertokens
161+
* @param request the request that contains currency information
162+
* @param signer the web3 signer
163+
* @param amount to allow, defaults to max allowance
164+
* @returns
165+
*/
166+
export async function wrapUnderlyingToken(
167+
request: ClientTypes.IRequestData,
168+
signer: Signer,
169+
amount: BigNumber = MAX_ALLOWANCE,
170+
): Promise<ContractTransaction> {
171+
const senderAddress = await signer.getAddress();
172+
const provider = signer.provider ?? getProvider();
173+
if (!(await checkSuperTokenUnderlyingAllowance(request, senderAddress, provider, amount))) {
174+
throw new Error('Supertoken not allowed to wrap this amount of underlying');
175+
}
176+
if (!(await hasEnoughUnderlyingToken(request, senderAddress, provider, amount))) {
177+
throw new Error('Sender does not have enough underlying token');
178+
}
179+
const wrapOp = await getWrapUnderlyingTokenOp(request, signer.provider ?? getProvider(), amount);
180+
return wrapOp.exec(signer);
181+
}
182+
183+
/**
184+
* Unwrap the supertoken (ERC777) into underlying asset (ERC20)
185+
* @param request the request that contains currency information
186+
* @param signer the web3 signer
187+
* @param amount to unwrap
188+
*/
189+
export async function unwrapSuperToken(
190+
request: ClientTypes.IRequestData,
191+
signer: Signer,
192+
amount: BigNumber,
193+
): Promise<ContractTransaction> {
194+
const sf = await getSuperFluidFramework(request, signer.provider ?? getProvider());
195+
const superToken = await sf.loadSuperToken(request.currencyInfo.value);
196+
const underlyingToken = await getRequestUnderlyingToken(
197+
request,
198+
signer.provider ?? getProvider(),
199+
);
200+
201+
if (underlyingToken.address === superToken.address) {
202+
throw new Error('This is a native super token');
203+
}
204+
205+
const userAddress = await signer.getAddress();
206+
const userBalance = await superToken.balanceOf({
207+
account: userAddress,
208+
providerOrSigner: signer.provider ?? getProvider(),
209+
});
210+
if (amount.gt(userBalance)) {
211+
throw new Error('Sender does not have enough supertoken');
212+
}
213+
const downgradeOp = superToken.downgrade({
214+
amount: amount.toString(),
215+
});
216+
return downgradeOp.exec(signer);
217+
}

packages/payment-processor/src/payment/utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import { getCurrencyHash } from '@requestnetwork/currency';
1111
import { ERC20__factory } from '@requestnetwork/smart-contracts/types';
1212
import { getPaymentNetworkExtension } from '@requestnetwork/payment-detection';
1313

14+
/** @constant MAX_ALLOWANCE set to the max uint256 value */
15+
export const MAX_ALLOWANCE = BigNumber.from(2).pow(256).sub(1);
16+
1417
/**
1518
* Thrown when the library does not support a payment blockchain network.
1619
*/

packages/payment-processor/test/payment/any-to-erc20-proxy.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { currencyManager } from './shared';
1717
import { IConversionPaymentSettings } from '../../src/index';
1818
import { UnsupportedCurrencyError } from '@requestnetwork/currency';
1919
import { AnyToERC20PaymentDetector } from '@requestnetwork/payment-detection';
20-
import { getProxyAddress, revokeErc20Approval } from '../../src/payment/utils';
20+
import { getProxyAddress, MAX_ALLOWANCE, revokeErc20Approval } from '../../src/payment/utils';
2121

2222
// Cf. ERC20Alpha in TestERC20.sol
2323
const erc20ContractAddress = '0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35';
@@ -27,7 +27,7 @@ const alphaPaymentSettings: IConversionPaymentSettings = {
2727
value: erc20ContractAddress,
2828
network: 'private',
2929
},
30-
maxToSpend: BigNumber.from(2).pow(256).sub(1),
30+
maxToSpend: MAX_ALLOWANCE,
3131
currencyManager,
3232
};
3333

packages/payment-processor/test/payment/encoder-approval.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
RequestLogicTypes,
88
} from '@requestnetwork/types';
99
import { encodeRequestErc20ApprovalIfNeeded } from '../../src';
10-
import { getProxyAddress, revokeErc20Approval } from '../../src/payment/utils';
10+
import { getProxyAddress, MAX_ALLOWANCE, revokeErc20Approval } from '../../src/payment/utils';
1111
import { AnyToERC20PaymentDetector, Erc20PaymentNetwork } from '@requestnetwork/payment-detection';
1212
import { currencyManager } from './shared';
1313
import { IPreparedTransaction } from 'payment-processor/dist/payment/prepared-transaction';
@@ -30,7 +30,7 @@ const alphaConversionSettings = {
3030
value: alphaContractAddress,
3131
network: 'private',
3232
},
33-
maxToSpend: BigNumber.from(2).pow(256).sub(1),
33+
maxToSpend: MAX_ALLOWANCE,
3434
currencyManager,
3535
};
3636
const alphaSwapSettings = {

packages/payment-processor/test/payment/encoder-payment.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
RequestLogicTypes,
88
} from '@requestnetwork/types';
99
import { encodeRequestPayment, encodeRequestPaymentWithStream } from '../../src';
10-
import { getProxyAddress } from '../../src/payment/utils';
10+
import { getProxyAddress, MAX_ALLOWANCE } from '../../src/payment/utils';
1111
import {
1212
AnyToERC20PaymentDetector,
1313
AnyToEthFeeProxyPaymentDetector,
@@ -36,7 +36,7 @@ const alphaConversionSettings = {
3636
value: alphaContractAddress,
3737
network: 'private',
3838
},
39-
maxToSpend: BigNumber.from(2).pow(256).sub(1),
39+
maxToSpend: MAX_ALLOWANCE,
4040
currencyManager,
4141
};
4242
const ethConversionSettings = {

0 commit comments

Comments
 (0)