Skip to content

Commit

Permalink
Refactor the requestRedemption fn
Browse files Browse the repository at this point in the history
We do not want to expose the low-level function from chain handles(see
`buildRedemptionData` from `EthereumBridge` handle). The goal of
`src/ethereum.ts` is to translate domain-level objects into low-level
Ethereum chain primitives and perform a call.

Instead of having `buildRedemptionData` in `Bridge` interface here we
modify the `approveAndCall` interface and replace `extraData: Hex` with
domain-level objects, as accepted by `buildRedemptionData`. Here we also
rename the `approveAndCall` to `requestRedemption` and explain in the
docs it happens via `approveAndCall`- although `requestRedemption` does
not exist on the Solidity code of TBTC token, having it in `TBTCToken`
Typescript API is fine given we abstract the complexity and make the API
human-readable.
  • Loading branch information
r-czajkowski committed Jul 4, 2023
1 parent 759cc78 commit 9249ee5
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 142 deletions.
73 changes: 41 additions & 32 deletions typescript/src/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,26 +244,6 @@ export interface Bridge {
* Returns the attached WalletRegistry instance.
*/
walletRegistry(): Promise<WalletRegistry>

/**
* Builds the redemption data required to request a redemption via
* @see TBTCToken#approveAndCall - the built data should be passed as
* `extraData` the @see TBTCToken#approveAndCall function.
* @param redeemer On-chain identifier of the redeemer.
* @param walletPublicKey The Bitcoin public key of the wallet. Must be in the
* compressed form (33 bytes long with 02 or 03 prefix).
* @param mainUtxo The main UTXO of the wallet. Must match the main UTXO
* held by the on-chain Bridge contract.
* @param redeemerOutputScript The output script that the redeemed funds will
* be locked to. Must be un-prefixed and not prepended with length.
* @returns The
*/
buildRedemptionData(
redeemer: Identifier,
walletPublicKey: string,
mainUtxo: UnspentTransactionOutput,
redeemerOutputScript: string
): Hex
}

/**
Expand Down Expand Up @@ -401,6 +381,40 @@ export interface TBTCVault {
getOptimisticMintingFinalizedEvents: GetEvents.Function<OptimisticMintingFinalizedEvent>
}

/**
* Represnts data required to request redemption.
*/
export interface RequestRedemptionData {
/**
* On-chain identifier of the redeemer.
*/
redeemer: Identifier
/**
* The Bitcoin public key of the wallet. Must be in the compressed form (33
* bytes long with 02 or 03 prefix).
*/
walletPublicKey: string
/**
* The main UTXO of the wallet. Must match the main UTXO held by the on-chain
* Bridge contract.
*/
mainUtxo: UnspentTransactionOutput
/**
* The output script that the redeemed funds will be locked to. Must be
* un-prefixed and not prepended with length.
*/
redeemerOutputScript: string
/**
* The amount to be redeemed with the precision of the tBTC on-chain token
* contract.
*/
amount: BigNumber
/**
* The vault address.
*/
vault: Identifier
}

/**
* Interface for communication with the TBTC v2 token on-chain contract.
*/
Expand All @@ -418,18 +432,13 @@ export interface TBTCToken {
totalSupply(blockNumber?: number): Promise<BigNumber>

/**
* Calls `receiveApproval` function on spender previously approving the spender
* to withdraw from the caller multiple times, up to the `amount` amount. If
* this function is called again, it overwrites the current allowance with
* `amount`.
* @param spender Address of contract authorized to spend.
* @param amount The max amount they can spend.
* @param extraData Extra information to send to the approved contract.
* Requests redemption in one transacion using the `approveAndCall` function
* from the tBTC on-chain token contract. Then the tBTC token contract calls
* the `receiveApproval` function from the `TBTCVault` contract which burns
* tBTC tokens and requests redemption.
* @param requestRedemptionData Data required to request redemption @see
* {@link RequestRedemptionData}.
* @returns Transaction hash of the approve and call transaction.
*/
approveAndCall(
spender: Identifier,
amount: BigNumber,
extraData: Hex
): Promise<Hex>
requestRedemption(requestRedemptionData: RequestRedemptionData): Promise<Hex>
}
148 changes: 74 additions & 74 deletions typescript/src/ethereum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
TBTCToken as ChainTBTCToken,
Identifier as ChainIdentifier,
GetEvents,
RequestRedemptionData,
} from "./chain"
import {
BigNumber,
Expand Down Expand Up @@ -488,39 +489,6 @@ export class Bridge
redeemerOutputScript: string,
amount: BigNumber
): Promise<void> {
const redemptionData = this.parseRequestRedemptionTransactionData(
walletPublicKey,
mainUtxo,
redeemerOutputScript
)

await sendWithRetry<ContractTransaction>(async () => {
return await this._instance.requestRedemption(
redemptionData.walletPublicKeyHash,
redemptionData.mainUtxo,
redemptionData.prefixedRawRedeemerOutputScript,
amount
)
}, this._totalRetryAttempts)
}

/**
* Parses the request redemption data to the proper form.
* @param walletPublicKey - The Bitcoin public key of the wallet. Must be in
* the compressed form (33 bytes long with 02 or 03 prefix).
* @param mainUtxo - The main UTXO of the wallet. Must match the main UTXO
* held by the on-chain contract.
* @param redeemerOutputScript - The output script that the redeemed funds
* will be locked to. Must be un-prefixed and not prepended with
* length.
* @returns Parsed data that can be passed to the contract to request
* redemption.
*/
private parseRequestRedemptionTransactionData = (
walletPublicKey: string,
mainUtxo: UnspentTransactionOutput,
redeemerOutputScript: string
) => {
const walletPublicKeyHash = `0x${computeHash160(walletPublicKey)}`

const mainUtxoParam = {
Expand All @@ -539,11 +507,14 @@ export class Bridge
rawRedeemerOutputScript,
]).toString("hex")}`

return {
walletPublicKeyHash,
mainUtxo: mainUtxoParam,
prefixedRawRedeemerOutputScript,
}
await sendWithRetry<ContractTransaction>(async () => {
return await this._instance.requestRedemption(
walletPublicKeyHash,
mainUtxoParam,
prefixedRawRedeemerOutputScript,
amount
)
}, this._totalRetryAttempts)
}

// eslint-disable-next-line valid-jsdoc
Expand Down Expand Up @@ -724,37 +695,6 @@ export class Bridge
signerOrProvider: this._instance.signer || this._instance.provider,
})
}

// eslint-disable-next-line valid-jsdoc
/**
* @see {ChainBridge#buildRedemptionData}
*/
buildRedemptionData(
redeemer: ChainIdentifier,
walletPublicKey: string,
mainUtxo: UnspentTransactionOutput,
redeemerOutputScript: string
): Hex {
const redemptionData = this.parseRequestRedemptionTransactionData(
walletPublicKey,
mainUtxo,
redeemerOutputScript
)

return Hex.from(
utils.defaultAbiCoder.encode(
["address", "bytes20", "bytes32", "uint32", "uint64", "bytes"],
[
redeemer.identifierHex,
redemptionData.walletPublicKeyHash,
redemptionData.mainUtxo.txHash,
redemptionData.mainUtxo.txOutputIndex,
redemptionData.mainUtxo.txOutputValue,
redemptionData.prefixedRawRedeemerOutputScript,
]
)
)
}
}

/**
Expand Down Expand Up @@ -1161,19 +1101,79 @@ export class TBTCToken
})
}

async approveAndCall(
spender: ChainIdentifier,
amount: BigNumber,
extraData: Hex
// eslint-disable-next-line valid-jsdoc
/**
* @see {ChainTBTCToken#requestRedemption}
*/
async requestRedemption(
requestRedemptionData: RequestRedemptionData
): Promise<Hex> {
const { vault, amount, ...restData } = requestRedemptionData

const extraData = this.buildRequestRedemptionData(restData)

const tx = await sendWithRetry<ContractTransaction>(async () => {
return await this._instance.approveAndCall(
spender.identifierHex,
vault.identifierHex,
amount,
extraData.toPrefixedString()
)
}, this._totalRetryAttempts)

return Hex.from(tx.hash)
}

private buildRequestRedemptionData(
requestRedemptionData: Omit<RequestRedemptionData, "amount" | "vault">
): Hex {
const { redeemer, ...restData } = requestRedemptionData
const { walletPublicKeyHash, mainUtxo, prefixedRawRedeemerOutputScript } =
this.buildBridgeRequestRedemptionData(restData)

return Hex.from(
utils.defaultAbiCoder.encode(
["address", "bytes20", "bytes32", "uint32", "uint64", "bytes"],
[
redeemer.identifierHex,
walletPublicKeyHash,
mainUtxo.txHash,
mainUtxo.txOutputIndex,
mainUtxo.txOutputValue,
prefixedRawRedeemerOutputScript,
]
)
)
}

private buildBridgeRequestRedemptionData(
data: Pick<
RequestRedemptionData,
"mainUtxo" | "walletPublicKey" | "redeemerOutputScript"
>
) {
const { walletPublicKey, mainUtxo, redeemerOutputScript } = data
const walletPublicKeyHash = `0x${computeHash160(walletPublicKey)}`

const mainUtxoParam = {
// The Ethereum Bridge expects this hash to be in the Bitcoin internal
// byte order.
txHash: mainUtxo.transactionHash.reverse().toPrefixedString(),
txOutputIndex: mainUtxo.outputIndex,
txOutputValue: mainUtxo.value,
}

// Convert the output script to raw bytes buffer.
const rawRedeemerOutputScript = Buffer.from(redeemerOutputScript, "hex")
// Prefix the output script bytes buffer with 0x and its own length.
const prefixedRawRedeemerOutputScript = `0x${Buffer.concat([
Buffer.from([rawRedeemerOutputScript.length]),
rawRedeemerOutputScript,
]).toString("hex")}`

return {
walletPublicKeyHash,
mainUtxo: mainUtxoParam,
prefixedRawRedeemerOutputScript,
}
}
}
12 changes: 5 additions & 7 deletions typescript/src/redemption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ export interface RedemptionRequest {
* @param amount - The amount to be redeemed with the precision of the tBTC
* on-chain token contract.
* @param vault - The vault address.
* @param bridge - Handle to the Bridge on-chain contract.
* @param tBTCToken - Handle to the TBTCToken on-chain contract.
* @returns Transaction hash of the request redemption transaction.
*/
Expand All @@ -77,17 +76,16 @@ export async function requestRedemption(
redeemerOutputScript: string,
amount: BigNumber,
vault: Identifier,
bridge: Bridge,
tBTCToken: TBTCToken
): Promise<Hex> {
const redemptionData = bridge.buildRedemptionData(
return await tBTCToken.requestRedemption({
redeemer,
walletPublicKey,
mainUtxo,
redeemerOutputScript
)

return await tBTCToken.approveAndCall(vault, amount, redemptionData)
redeemerOutputScript,
amount,
vault,
})
}

/**
Expand Down
23 changes: 9 additions & 14 deletions typescript/test/redemption.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ describe("Redemption", () => {
const redeemerOutputScript =
data.pendingRedemptions[0].pendingRedemption.redeemerOutputScript
const amount = data.pendingRedemptions[0].pendingRedemption.requestedAmount
const bridge: MockBridge = new MockBridge()
const vault = Address.from("0xb622eA9D678ddF15135a20d59Ff26D28eC246bfB")
const token: MockTBTCToken = new MockTBTCToken()
const redeemer = Address.from("0x117284D8C50f334a1E2b7712649cB23C7a04Ae74")
Expand All @@ -61,26 +60,22 @@ describe("Redemption", () => {
redeemerOutputScript,
amount,
vault,
bridge,
token
)
})

it("should submit redemption proof with correct arguments", () => {
const bridgeLog = bridge.buildRedemptionDataLog
const tokenLog = token.approveAndCallLog

expect(bridgeLog.length).to.equal(1)
expect(bridgeLog[0].walletPublicKey).to.equal(
redemptionProof.expectedRedemptionProof.walletPublicKey
)
expect(bridgeLog[0].mainUtxo).to.equal(mainUtxo)
expect(bridgeLog[0].redeemerOutputScript).to.equal(redeemerOutputScript)
expect(bridgeLog[0].redeemer).to.equal(redeemer)
const tokenLog = token.requestRedemptionLog

expect(tokenLog.length).to.equal(1)
expect(tokenLog[0].spender).to.equal(vault)
expect(tokenLog[0].amount).to.equal(amount)
expect(tokenLog[0]).to.deep.equal({
redeemer,
walletPublicKey,
mainUtxo,
redeemerOutputScript,
amount,
vault,
})
})
})

Expand Down
Loading

0 comments on commit 9249ee5

Please sign in to comment.