Skip to content

Commit

Permalink
feat(unlockjs): support tx building to return only calldata (#15136)
Browse files Browse the repository at this point in the history
* factor params

* calldata only purchase

* add support for purchaseKey and purchaseKeys

* pass provider explicitely

* bind self object to prepare purchase helper

* allowance pass signer if exists

* update README
  • Loading branch information
clemsos authored Dec 2, 2024
1 parent 2ca0c0e commit 6db1d9c
Show file tree
Hide file tree
Showing 13 changed files with 389 additions and 135 deletions.
14 changes: 6 additions & 8 deletions packages/unlock-js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,14 @@ You can run the entire test suite using
yarn test
```

Run a single test
### Run a single integration test using a specific versions

You can edit `testPath` and versions values in `./src/__integrations__/single.js` to test for specific functions on specific versions of the protocol.
You can use env variables to specify which test to run. Alternatively, you can edit path and versions directly in `./src/__integrations__/single.js` to test for a specific functions on specific versions of the protocol

```
yarn test:single
```

Run a single integration test with specific versions
export UNLOCK_JS_TEST_RUN_UNLOCK_VERSION='v13'
export UNLOCK_JS_TEST_RUN_PUBLIC_LOCK_VERSION='v14'
export UNLOCK_JS_TEST_RUN_TEST_PATH='./lock/purchaseKeys.js'
```
yarn hardhat test:integration src/__tests__/integration/lock/cancelAndRefund.js --unlock-version 10 --lock-version 12
yarn test:single
```
14 changes: 12 additions & 2 deletions packages/unlock-js/src/PublicLock/utils/approveAllowance.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@
import { ZERO } from '../../constants'
import { approveTransfer, getAllowance } from '../../erc20'
import { approveTransfer, getAllowance, prepareApproval } from '../../erc20'
import utils from '../../utils'

export default async function approveAllowance({
erc20Address,
address,
totalAmountToApprove,
onlyData,
}) {
if (erc20Address && erc20Address !== ZERO) {
const approvedAmount = await getAllowance(
erc20Address,
address,
this.provider,
this.signer.getAddress()
this.signer ? this.signer.getAddress() : null
)

if (
!approvedAmount ||
utils.bigNumberify(approvedAmount) < totalAmountToApprove
) {
// get only tx data
if (onlyData) {
const data = await prepareApproval(address, totalAmountToApprove)
return {
data,
to: erc20Address,
value: 0,
}
}
// We must wait for the transaction to pass if we want estimates for the next one to succeed!
await (
await approveTransfer(
Expand Down
20 changes: 15 additions & 5 deletions packages/unlock-js/src/PublicLock/v10/getPurchaseKeysArguments.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import utils from '../../utils'
import formatKeyPrice from '../utils/formatKeyPrice'

export default async function getPurchaseKeysArguments({
lockContract,
owners: _owners,
keyManagers: _keyManagers,
keyPrices: _keyPrices,
Expand All @@ -14,16 +15,25 @@ export default async function getPurchaseKeysArguments({
totalApproval, // explicit approval amount
data: _data,
}) {
const lockContract = await this.getLockContract(lockAddress)

// If erc20Address was not provided, get it
if (!erc20Address) {
erc20Address = await lockContract.tokenAddress()
}

// owners default to a single key for current signer
const defaultOwner = await this.signer.getAddress()
const owners = _owners || [defaultOwner]
let owners
if (!_owners || (_owners || []).length === 0) {
if (this.signer) {
// owners default to a single key for current signer
const defaultOwner = await this.signer.getAddress()
owners = [defaultOwner]
} else {
throw Error(
'Missing recipients. You need to specify explicit key owners when using Web3Service to generate calldata'
)
}
} else {
owners = _owners
}

// we parse by default a length corresponding to the owners length
const defaultArray = Array(owners.length).fill(null)
Expand Down
4 changes: 4 additions & 0 deletions packages/unlock-js/src/PublicLock/v10/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import getLock from './getLock'
import renewMembershipFor from './renewMembershipFor'
import v9 from '../v9'
import getPurchaseKeysArguments from './getPurchaseKeysArguments'
import preparePurchaseKeysTx from './preparePurchaseKeysTx'
import preparePurchaseKeyTx from './preparePurchaseKeyTx'

const {
grantKey,
Expand Down Expand Up @@ -85,4 +87,6 @@ export default {
setKeyManagerOf,
transferFrom,
setGasRefundValue,
preparePurchaseKeyTx,
preparePurchaseKeysTx,
}
41 changes: 41 additions & 0 deletions packages/unlock-js/src/PublicLock/v10/preparePurchaseKeyTx.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import preparePurchaseKeysTx from './preparePurchaseKeysTx'

/**
* Build tx for purchasing a single key
* @returns {object}
* - {PropTypes.address} to
* - {PropTypes.number} value
* - {PropTypes.bytes} data
*/
export default async function (
{
lockAddress,
owner,
keyManager,
keyPrice,
erc20Address,
decimals,
referrer,
recurringPayments, // nb of reccuring payments to approve,
totalApproval, // Explicit approval amount
data,
},
provider
) {
const txs = await preparePurchaseKeysTx.bind(this)(
{
owners: owner ? [owner] : null,
keyManagers: keyManager ? [keyManager] : null,
keyPrices: keyPrice ? [keyPrice] : null,
referrers: referrer ? [referrer] : null,
data: data ? [data] : null,
recurringPayments: recurringPayments ? [recurringPayments] : null,
lockAddress,
erc20Address,
totalApproval,
decimals,
},
provider
)
return txs
}
69 changes: 69 additions & 0 deletions packages/unlock-js/src/PublicLock/v10/preparePurchaseKeysTx.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { ZERO } from '../../constants'
import getPurchaseKeysArguments from './getPurchaseKeysArguments'
import approveAllowance from '../utils/approveAllowance'

/**
* This function will build a purchase tx based on the params
* and return from, to, value, data so it can be sent directly
* via a provider.
* @param {object} params:
* - {PropTypes.arrayOf(PropTypes.address)} lockAddress
* - {PropTypes.arrayOf(PropTypes.address)} owners
* - {PropTypes.arrayOf(string)} keyPrices
* - {PropTypes.address} erc20Address
* - {number} decimals
* - {PropTypes.arrayOf(PropTypes.address)} referrers (address which will receive UDT - if applicable)
* - {PropTypes.arrayOf(number)} recurringPayments the number of payments to allow for each keys. If the array is set, the keys are considered using recurring ERRC20 payments).
* - {PropTypes.arrayOf(PropTypes.array[bytes])} _data (array of array of bytes, not used in transaction but can be used by hooks)
* */
export default async function preparePurchase(options, provider) {
const { lockAddress } = options
const lockContract = await this.getLockContract(lockAddress, provider)
options.lockContract = lockContract
const {
owners,
keyPrices,
keyManagers,
referrers,
data,
totalPrice,
erc20Address,
totalAmountToApprove,
} = await getPurchaseKeysArguments.bind(this)(options)

const txs = []

// If the lock is priced in ERC20, we need to approve the transfer
const approvalOptions = {
erc20Address,
totalAmountToApprove,
address: lockAddress,
onlyData: true,
}

// Only ask for approval if the lock is priced in ERC20
if (
approvalOptions.erc20Address &&
approvalOptions.erc20Address !== ZERO &&
totalAmountToApprove > 0
) {
const approvalTxRequest = await approveAllowance.bind(this)(approvalOptions)
txs.push(approvalTxRequest)
}

// parse
const purchaseArgs = [keyPrices, owners, referrers, keyManagers, data]
const callData = lockContract.interface.encodeFunctionData(
'purchase',
purchaseArgs
)

const value = !erc20Address || erc20Address === ZERO ? totalPrice : 0
const purchaseTxRequest = {
data: callData,
value,
to: lockAddress,
}
txs.push(purchaseTxRequest)
return txs
}
75 changes: 16 additions & 59 deletions packages/unlock-js/src/PublicLock/v10/purchaseKeys.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ZERO } from '../../constants'
import getPurchaseKeysArguments from './getPurchaseKeysArguments'
import preparePurchaseTx from './preparePurchaseKeysTx'
import approveAllowance from '../utils/approveAllowance'

/**
Expand All @@ -18,42 +18,17 @@ import approveAllowance from '../utils/approveAllowance'
export default async function (options, transactionOptions = {}, callback) {
const { lockAddress } = options
const lockContract = await this.getLockContract(lockAddress)
const {
owners,
keyPrices,
keyManagers,
referrers,
data,
totalPrice,
erc20Address,
totalAmountToApprove,
} = await getPurchaseKeysArguments.bind(this)(options)

const purchaseArgs = [keyPrices, owners, referrers, keyManagers, data]
const callData = lockContract.interface.encodeFunctionData(
'purchase',
purchaseArgs
)

// tx options
if (!erc20Address || erc20Address === ZERO) {
transactionOptions.value = totalPrice
}
// get the tx data
const txRequests = await preparePurchaseTx.bind(this)(options, this.provider)

// If the lock is priced in ERC20, we need to approve the transfer
const approvalOptions = {
erc20Address,
totalAmountToApprove,
address: lockAddress,
}

// Only ask for approval if the lock is priced in ERC20
if (
approvalOptions.erc20Address &&
approvalOptions.erc20Address !== ZERO &&
totalAmountToApprove > 0
) {
await approveAllowance.bind(this)(approvalOptions)
let approvalTransactionRequest, purchaseTransactionRequest
if (txRequests.length === 2) {
// execute approval if necessary
;[approvalTransactionRequest, purchaseTransactionRequest] = txRequests
await this.signer.sendTransaction(approvalTransactionRequest)
} else {
;[purchaseTransactionRequest] = txRequests
}

// Estimate gas. Bump by 30% because estimates are wrong!
Expand All @@ -74,17 +49,7 @@ export default async function (options, transactionOptions = {}, callback) {
transactionOptions.gasPrice = gasPrice
}
}

const gasLimitPromise = lockContract.purchase.estimateGas(
keyPrices,
owners,
referrers,
keyManagers,
data,
transactionOptions
)

const gasLimit = await gasLimitPromise
const gasLimit = this.signer.estimateGas(purchaseTransactionRequest)
transactionOptions.gasLimit = (gasLimit * 13n) / 10n
} catch (error) {
console.error(
Expand All @@ -99,25 +64,17 @@ export default async function (options, transactionOptions = {}, callback) {
}
}

const transactionRequestPromise = lockContract.purchase.populateTransaction(
keyPrices,
owners,
referrers,
keyManagers,
data,
transactionOptions
)

const transactionRequest = await transactionRequestPromise
if (transactionOptions.runEstimate) {
const estimate = this.signer.estimateGas(transactionRequest)
const estimate = this.signer.estimateGas(purchaseTransactionRequest)
return {
transactionRequest,
purchaseTransactionRequest,
estimate,
}
}

const transactionPromise = this.signer.sendTransaction(transactionRequest)
const transactionPromise = this.signer.sendTransaction(
purchaseTransactionRequest
)

const hash = await this._handleMethodCall(transactionPromise)

Expand Down
Loading

0 comments on commit 6db1d9c

Please sign in to comment.