@xlink-network/xlink-sdk
Version:
182 lines (170 loc) • 5.54 kB
text/typescript
import { Client, encodeFunctionData, zeroAddress } from "viem"
import { estimateGas, readContract } from "viem/actions"
import { BridgeEndpointAbi } from "../evmUtils/contractAbi/bridgeEndpoint"
import { BridgeTimeLockAbi } from "../evmUtils/contractAbi/bridgeTimeLock"
import {
getEVMContractCallInfo,
getEVMTokenContractInfo,
numberFromSolidityContractNumber,
} from "../evmUtils/xlinkContractHelpers"
import { BigNumber } from "../utils/BigNumber"
import { UnsupportedChainError } from "../utils/errors"
import { decodeHex } from "../utils/hexHelpers"
import { isNotNull } from "../utils/typeHelpers"
import {
KnownChainId,
KnownTokenId,
_allKnownEVMTokens,
} from "../utils/types/knownIds"
import { EVMAddress, SDKNumber, toSDKNumberOrUndefined } from "./types"
import { SDKGlobalContext } from "./types.internal"
import { nativeCurrencyAddress } from "../evmUtils/addressHelpers"
export interface TimeLockedAsset {
id: string
chain: KnownChainId.EVMChain
token: KnownTokenId.EVMToken
amount: SDKNumber
releaseTime: Date
}
const getTimeLockContractCallInfo = async (
ctx: SDKGlobalContext,
chain: KnownChainId.EVMChain,
): Promise<
| undefined
| {
client: Client
timeLockContractAddress: EVMAddress
}
> => {
const info = await getEVMContractCallInfo(ctx, chain)
if (info == null) return
const timeLockContractAddress =
info.timeLockContractAddress ??
(await readContract(info.client, {
abi: BridgeEndpointAbi,
address: info.bridgeEndpointContractAddress,
functionName: "timeLock",
}).catch(err => {
console.groupCollapsed(
`Failed to read timeLock contract address from ${info.bridgeEndpointContractAddress} (${chain})`,
)
console.debug(err)
console.groupEnd()
return zeroAddress
}))
if (timeLockContractAddress === zeroAddress) return
return { client: info.client, timeLockContractAddress }
}
export interface GetTimeLockedAssetsInput {
walletAddress: EVMAddress
chains: KnownChainId.EVMChain[]
}
export interface GetTimeLockedAssetsOutput {
assets: TimeLockedAsset[]
}
export async function getTimeLockedAssetsFromEVM(
ctx: SDKGlobalContext,
input: GetTimeLockedAssetsInput,
): Promise<GetTimeLockedAssetsOutput> {
const promises = input.chains.map(async chain => {
const timeLockCallInfo = await getTimeLockContractCallInfo(ctx, chain)
if (timeLockCallInfo == null) return []
const tokenCallInfos = (
await Promise.all(
_allKnownEVMTokens.map(token =>
getEVMTokenContractInfo(ctx, chain, token).then(info =>
info == null || info.tokenContractAddress === nativeCurrencyAddress
? null
: { ...info, chain, token },
),
),
)
).filter(isNotNull)
const agreements = (
await Promise.all(
tokenCallInfos.map(info =>
readContract(timeLockCallInfo.client, {
abi: BridgeTimeLockAbi,
address: timeLockCallInfo.timeLockContractAddress,
functionName: "agreementsByUser",
args: [input.walletAddress, 0, info.tokenContractAddress, "0x"],
}).then(agreementId =>
agreementId === 0n ? [] : [{ agreementId, info }],
),
),
)
).flat()
return Promise.all(
agreements.map(async ({ agreementId, info }) => {
const agreement = await readContract(timeLockCallInfo.client, {
abi: BridgeTimeLockAbi,
address: timeLockCallInfo.timeLockContractAddress,
functionName: "agreements",
args: [agreementId],
})
return {
id: String(agreementId),
chain: info.chain,
token: info.token,
amount: toSDKNumberOrUndefined(
numberFromSolidityContractNumber(agreement[0]),
),
releaseTime: new Date(agreement[6] * 1000),
}
}),
)
})
return {
assets: (await Promise.all(promises)).flat(),
}
}
export interface ClaimTimeLockedAssetsInput {
chain: KnownChainId.EVMChain
walletAddress: EVMAddress
lockedAssetIds: string[]
sendTransaction: (tx: {
from: EVMAddress
to: EVMAddress
data: Uint8Array
recommendedGasLimit: SDKNumber
}) => Promise<{
txHash: string
}>
}
export interface ClaimTimeLockedAssetsOutput {
txHash: string
}
export const claimTimeLockedAssetsFromEVM = async (
ctx: SDKGlobalContext,
input: ClaimTimeLockedAssetsInput,
): Promise<undefined | ClaimTimeLockedAssetsOutput> => {
const info = await getTimeLockContractCallInfo(ctx, input.chain)
if (info == null) throw new UnsupportedChainError(input.chain)
const functionData = encodeFunctionData({
abi: BridgeTimeLockAbi,
functionName: "claim",
args: [
input.lockedAssetIds.map(id =>
BigNumber.toBigInt({ roundingMode: BigNumber.roundDown }, id),
),
],
})
const estimated = await estimateGas(info.client, {
account: input.walletAddress,
to: info.timeLockContractAddress,
data: functionData,
})
.then(n => BigNumber.round({ precision: 0 }, BigNumber.mul(n, 1.2)))
.catch(
// add a fallback in case estimate failed
() =>
// https://bscscan.com/tx/0x28a81312ca7bc93e7ef07867c9906a41b251ea3ea630b0a4837bdb3066489b32
1 * 1e6,
)
return await input.sendTransaction({
from: input.walletAddress,
to: info.timeLockContractAddress,
data: decodeHex(functionData),
recommendedGasLimit: toSDKNumberOrUndefined(estimated),
})
}