From 7b33e2daf5ba2321c402cfcceb877b9807e05f0b Mon Sep 17 00:00:00 2001 From: Kai Hirota <34954529+kaihirota@users.noreply.github.com> Date: Wed, 28 Feb 2024 11:08:43 +1100 Subject: [PATCH] CORE-2034 add register and complete withdraw workflow (#438) * add register and complete withdraw workflow * changelog and version --- CHANGELOG.md | 13 ++ package.json | 2 +- .../withdrawal/completeERC20Withdrawal.ts | 37 ++++- .../withdrawal/completeERC721Withdrawal.ts | 129 +++++++++++++++++- .../withdrawal/completeEthWithdrawal.ts | 33 +++++ .../withdrawal/completeWithdrawal.ts | 46 ++++++- src/workflows/withdrawal/index.ts | 2 + src/workflows/workflows.ts | 12 +- 8 files changed, 267 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c0caa5c..f8cb8b44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [3.5.0] - 2024-02-28 + +### Added + +- Update `completeWithdrawal`: + - If the user has only v4 balance, `completeWithdrawal` will withdraw v4 balance. + - If the user has v3 balance, `completeWithdrawal` will try to withdraw both v3 and v4 balance. + - If the user has v3 balance, and is not registered on-chain, `completeWithdrawal` will try to register the user and then withdraw both v3 and v4 balance. + - If the v3 balance is not zero for an NFT, `completeWithdrawal` will only try to withdraw v3 balance. +- v3 balance refers to withdrawal prepared using /v1/withdrawal API. +- v4 balance refers to withdrawal prepared using /v2/withdrawal API. + ## [3.4.0] - 2024-02-27 ### Added diff --git a/package.json b/package.json index 9f1c0cb3..35814ccd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imtbl/core-sdk", - "version": "3.4.0", + "version": "3.5.0", "description": "Immutable Core SDK", "main": "dist/index.cjs.js", "module": "dist/index.es.js", diff --git a/src/workflows/withdrawal/completeERC20Withdrawal.ts b/src/workflows/withdrawal/completeERC20Withdrawal.ts index ef877eff..c32cc6a9 100644 --- a/src/workflows/withdrawal/completeERC20Withdrawal.ts +++ b/src/workflows/withdrawal/completeERC20Withdrawal.ts @@ -10,10 +10,11 @@ import { Registration__factory, RegistrationV4__factory, } from '../../contracts'; -import { ERC20Token } from '../../types'; +import { ERC20Token, WalletConnection } from '../../types'; import { getSignableRegistrationOnchain, isRegisteredOnChainWorkflow, + signRegisterEthAddress, } from '../registration'; import { getEncodeAssetInfo } from './getEncodeAssetInfo'; @@ -153,3 +154,37 @@ export async function completeAllERC20WithdrawalWorkflow( return signer.sendTransaction(populatedTransaction); } + +export async function registerAndCompleteAllERC20WithdrawalWorkflow( + walletConnection: WalletConnection, + ethAddress: string, + starkPublicKey: string, + token: ERC20Token, + encodingApi: EncodingApi, + config: ImmutableXConfiguration, +): Promise { + const assetType = await getEncodeAssetInfo('asset', 'ERC20', encodingApi, { + token_address: token.tokenAddress, + }); + + const registrationContract = RegistrationV4__factory.connect( + config.ethConfiguration.registrationContractAddress, + walletConnection.ethSigner, + ); + + const starkSignature = await signRegisterEthAddress( + walletConnection.starkSigner, + ethAddress, + starkPublicKey, + ); + + const populatedTransaction = + await registrationContract.populateTransaction.registerAndWithdrawAll( + ethAddress, + starkPublicKey, + starkSignature, + assetType.asset_id, + ); + + return walletConnection.ethSigner.sendTransaction(populatedTransaction); +} diff --git a/src/workflows/withdrawal/completeERC721Withdrawal.ts b/src/workflows/withdrawal/completeERC721Withdrawal.ts index 68888e00..1739cf4a 100644 --- a/src/workflows/withdrawal/completeERC721Withdrawal.ts +++ b/src/workflows/withdrawal/completeERC721Withdrawal.ts @@ -3,16 +3,18 @@ import { EncodingApi, MintsApi, UsersApi } from '../../api'; import { Core, Core__factory, + CoreV4__factory, Registration, Registration__factory, - CoreV4__factory, + RegistrationV4__factory, } from '../../contracts'; import * as encUtils from 'enc-utils'; -import { ERC721Token } from '../../types'; +import { ERC721Token, WalletConnection } from '../../types'; import { getEncodeAssetInfo } from './getEncodeAssetInfo'; import { getSignableRegistrationOnchain, isRegisteredOnChainWorkflow, + signRegisterEthAddress, } from '../registration'; import { TransactionResponse } from '@ethersproject/providers'; import { ImmutableXConfiguration } from '../../config'; @@ -330,6 +332,72 @@ async function completeERC721WithdrawalV2( return signer.sendTransaction(populatedTransaction); } +async function registerAndCompleteMintableERC721WithdrawalV2( + signer: Signer, + ethAddress: string, + starkPublicKey: string, + starkSignature: string, + token: MintableERC721Withdrawal, + encodingApi: EncodingApi, + config: ImmutableXConfiguration, +) { + const assetType = await getEncodeAssetInfo( + 'mintable-asset', + 'ERC721', + encodingApi, + { + id: token.data.id, + token_address: token.data.tokenAddress, + ...(token.data.blueprint && { blueprint: token.data.blueprint }), + }, + ); + const mintingBlob = getMintingBlob(token); + const contract = RegistrationV4__factory.connect( + config.ethConfiguration.registrationContractAddress, + signer, + ); + + const populatedTransaction = + await contract.populateTransaction.registerWithdrawAndMint( + ethAddress, + starkPublicKey, + starkSignature, + assetType.asset_type, + mintingBlob, + ); + return signer.sendTransaction(populatedTransaction); +} + +async function registerAndCompleteERC721WithdrawalV2( + signer: Signer, + ethAddress: string, + starkPublicKey: string, + starkSignature: string, + token: ERC721Token, + encodingApi: EncodingApi, + config: ImmutableXConfiguration, +) { + const assetType = await getEncodeAssetInfo('asset', 'ERC721', encodingApi, { + token_id: token.tokenId, + token_address: token.tokenAddress, + }); + + const contract = RegistrationV4__factory.connect( + config.ethConfiguration.registrationContractAddress, + signer, + ); + + const populatedTransaction = + await contract.populateTransaction.registerAndWithdrawNft( + ethAddress, + starkPublicKey, + starkSignature, + assetType.asset_type, + token.tokenId, + ); + return signer.sendTransaction(populatedTransaction); +} + export async function completeERC721WithdrawalV2Workflow( signer: Signer, ownerKey: string, @@ -375,3 +443,60 @@ export async function completeERC721WithdrawalV2Workflow( throw error; // unable to recover from any other kind of error }); } + +export async function registerAndCompleteERC721WithdrawalWorkflow( + walletConnection: WalletConnection, + ethAddress: string, + starkPublicKey: string, + token: ERC721Token, + encodingApi: EncodingApi, + mintsApi: MintsApi, + config: ImmutableXConfiguration, +): Promise { + const starkSignature = await signRegisterEthAddress( + walletConnection.starkSigner, + ethAddress, + starkPublicKey, + ); + const tokenAddress = token.tokenAddress; + const tokenId = token.tokenId; + + return await mintsApi + .getMintableTokenDetailsByClientTokenId({ + tokenAddress, + tokenId, + }) + .then(async mintableToken => + registerAndCompleteMintableERC721WithdrawalV2( + walletConnection.ethSigner, + ethAddress, + starkPublicKey, + starkSignature, + { + type: 'ERC721', + data: { + id: tokenId, + tokenAddress: tokenAddress, + blueprint: mintableToken.data.blueprint, + }, + }, + encodingApi, + config, + ), + ) + .catch(error => { + if (error.response?.status === 404) { + // token is already minted on L1 + return registerAndCompleteERC721WithdrawalV2( + walletConnection.ethSigner, + ethAddress, + starkPublicKey, + starkSignature, + token, + encodingApi, + config, + ); + } + throw error; // unable to recover from any other kind of error + }); +} diff --git a/src/workflows/withdrawal/completeEthWithdrawal.ts b/src/workflows/withdrawal/completeEthWithdrawal.ts index 30463518..bdd71318 100644 --- a/src/workflows/withdrawal/completeEthWithdrawal.ts +++ b/src/workflows/withdrawal/completeEthWithdrawal.ts @@ -13,8 +13,10 @@ import { import { getSignableRegistrationOnchain, isRegisteredOnChainWorkflow, + signRegisterEthAddress, } from '../registration'; import { getEncodeAssetInfo } from './getEncodeAssetInfo'; +import { WalletConnection } from 'src/types'; async function executeRegisterAndWithdrawEth( signer: Signer, @@ -143,3 +145,34 @@ export async function completeAllEthWithdrawalWorkflow( return signer.sendTransaction(populatedTransaction); } + +export async function registerAndCompleteAllEthWithdrawalWorkflow( + walletConnection: WalletConnection, + ethAddress: string, + starkPublicKey: string, + encodingApi: EncodingApi, + config: ImmutableXConfiguration, +): Promise { + const assetType = await getEncodeAssetInfo('asset', 'ETH', encodingApi); + + const registrationContract = RegistrationV4__factory.connect( + config.ethConfiguration.registrationContractAddress, + walletConnection.ethSigner, + ); + + const starkSignature = await signRegisterEthAddress( + walletConnection.starkSigner, + ethAddress, + starkPublicKey, + ); + + const populatedTransaction = + await registrationContract.populateTransaction.registerAndWithdrawAll( + ethAddress, + starkPublicKey, + starkSignature, + assetType.asset_id, + ); + + return walletConnection.ethSigner.sendTransaction(populatedTransaction); +} diff --git a/src/workflows/withdrawal/completeWithdrawal.ts b/src/workflows/withdrawal/completeWithdrawal.ts index 3def5a6a..93520dd8 100644 --- a/src/workflows/withdrawal/completeWithdrawal.ts +++ b/src/workflows/withdrawal/completeWithdrawal.ts @@ -1,21 +1,24 @@ import { Signer } from '@ethersproject/abstract-signer'; import { TransactionResponse } from '@ethersproject/providers'; -import { AnyToken } from 'src/types'; +import { AnyToken, WalletConnection } from 'src/types'; import { EncodingApi, MintsApi, UsersApi } from 'src/api'; import { ImmutableXConfiguration } from 'src/config'; import { completeAllEthWithdrawalWorkflow, completeEthWithdrawalV1Workflow, completeEthWithdrawalV2Workflow, + registerAndCompleteAllEthWithdrawalWorkflow, } from './completeEthWithdrawal'; import { completeAllERC20WithdrawalWorkflow, completeERC20WithdrawalV1Workflow, completeERC20WithdrawalV2Workflow, + registerAndCompleteAllERC20WithdrawalWorkflow, } from './completeERC20Withdrawal'; import { completeERC721WithdrawalV1Workflow, completeERC721WithdrawalV2Workflow, + registerAndCompleteERC721WithdrawalWorkflow, } from './completeERC721Withdrawal'; export async function completeWithdrawalV1Workflow( @@ -124,3 +127,44 @@ export async function completeAllWithdrawalWorkflow( ); } } + +export async function registerAndCompleteAllWithdrawalWorkflow( + walletConnection: WalletConnection, + ethAddress: string, + starkPublicKey: string, + token: AnyToken, + encodingApi: EncodingApi, + mintsApi: MintsApi, + config: ImmutableXConfiguration, +): Promise { + switch (token.type) { + case 'ETH': + return registerAndCompleteAllEthWithdrawalWorkflow( + walletConnection, + ethAddress, + starkPublicKey, + encodingApi, + config, + ); + case 'ERC20': + return registerAndCompleteAllERC20WithdrawalWorkflow( + walletConnection, + ethAddress, + starkPublicKey, + token, + encodingApi, + config, + ); + case 'ERC721': + // for ERC721, if the v3 balance > 0, then the v4 balance is 0 + return registerAndCompleteERC721WithdrawalWorkflow( + walletConnection, + ethAddress, + starkPublicKey, + token, + encodingApi, + mintsApi, + config, + ); + } +} diff --git a/src/workflows/withdrawal/index.ts b/src/workflows/withdrawal/index.ts index 6496f6fa..36fae8fe 100644 --- a/src/workflows/withdrawal/index.ts +++ b/src/workflows/withdrawal/index.ts @@ -1,4 +1,6 @@ export * from './prepareWithdrawal'; +export * from './completeWithdrawal'; export * from './completeERC20Withdrawal'; export * from './completeERC721Withdrawal'; export * from './completeEthWithdrawal'; +export * from './getWithdrawalBalance'; diff --git a/src/workflows/workflows.ts b/src/workflows/workflows.ts index 15eda3c4..4c9da308 100644 --- a/src/workflows/workflows.ts +++ b/src/workflows/workflows.ts @@ -72,6 +72,7 @@ import { completeAllWithdrawalWorkflow, completeWithdrawalV1Workflow, completeWithdrawalV2Workflow, + registerAndCompleteAllWithdrawalWorkflow, } from './withdrawal/completeWithdrawal'; import { BigNumber } from 'ethers'; import { getWithdrawalBalanceWorkflow } from './withdrawal/getWithdrawalBalance'; @@ -311,7 +312,6 @@ export class Workflows { const starkPublicKey = await walletConnection.starkSigner.getAddress(); if (majorContractVersion === 3) { - const starkPublicKey = await walletConnection.starkSigner.getAddress(); return completeWithdrawalV1Workflow( walletConnection.ethSigner, starkPublicKey, @@ -359,7 +359,15 @@ export class Workflows { this.config, ); } - throw new Error('User unregistered'); + return registerAndCompleteAllWithdrawalWorkflow( + walletConnection, + ethAddress, + starkPublicKey, + token, + this.encodingApi, + this.mintsApi, + this.config, + ); } if (v4Balance.gt(0)) { return completeWithdrawalV2Workflow(