Skip to content

Commit e469abf

Browse files
feat(biz): Add Biz.signExecuteWithSignatureCall and BizPasskeySession.signExecuteWithSignatureCall (#4594)
* feat(biz): Add `Biz.SignExecuteWithSignatureCall` * Add new `BizPasskeySession` module * feat(biz): Add `Biz.SignExecuteWithSignatureCall` * Finish `BizPasskeySession` module separation * feat(biz): Fix android tests * feat(biz): Address PR review comments
1 parent 63fc782 commit e469abf

File tree

17 files changed

+1007
-376
lines changed

17 files changed

+1007
-376
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ include/TrustWalletCore/TWCryptoBoxSecretKey.h
5454
include/TrustWalletCore/TWEthereum.h
5555
include/TrustWalletCore/TWBarz.h
5656
include/TrustWalletCore/TWBiz.h
57+
include/TrustWalletCore/TWBizPasskeySession.h
5758
include/TrustWalletCore/TWEip7702.h
5859
include/TrustWalletCore/TWWebAuthnSolidity.h
5960

android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/ethereum/TestBiz.kt

Lines changed: 36 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -201,165 +201,45 @@ class TestBiz {
201201
}
202202

203203
@Test
204-
fun testBizEncodeRegisterSessionCall() {
205-
val publicKey = PublicKey(
206-
"0x041c05286fe694493eae33312f2d2e0d0abeda8db76238b7a204be1fb87f54ce4228fef61ef4ac300f631657635c28e59bfb2fe71bce1634c81c65642042f6dc4d".toHexByteArray(),
207-
PublicKeyType.NIST256P1EXTENDED
208-
)
209-
val validUntil = "0x15181" // 86_401
210-
val encoded = WCBiz.encodeRegisterSessionCall(publicKey, Numeric.hexStringToByteArray(validUntil))
211-
assertEquals(
212-
"0x826491fb000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000151810000000000000000000000000000000000000000000000000000000000000041041c05286fe694493eae33312f2d2e0d0abeda8db76238b7a204be1fb87f54ce4228fef61ef4ac300f631657635c28e59bfb2fe71bce1634c81c65642042f6dc4d00000000000000000000000000000000000000000000000000000000000000",
213-
Numeric.toHexString(encoded)
214-
)
215-
}
216-
217-
@Test
218-
fun testBizEncodeRemoveSessionCall() {
219-
val publicKey = PublicKey(
220-
"0x041c05286fe694493eae33312f2d2e0d0abeda8db76238b7a204be1fb87f54ce4228fef61ef4ac300f631657635c28e59bfb2fe71bce1634c81c65642042f6dc4d".toHexByteArray(),
221-
PublicKeyType.NIST256P1EXTENDED
222-
)
223-
val encoded = WCBiz.encodeRemoveSessionCall(publicKey)
224-
assertEquals(
225-
"0xe1c06abd00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000041041c05286fe694493eae33312f2d2e0d0abeda8db76238b7a204be1fb87f54ce4228fef61ef4ac300f631657635c28e59bfb2fe71bce1634c81c65642042f6dc4d00000000000000000000000000000000000000000000000000000000000000",
226-
Numeric.toHexString(encoded)
227-
)
228-
}
229-
230-
@Test
231-
fun testBizEncodePasskeyNonce() {
232-
val nonce = "0x7b" // 123
233-
val passkeyNonce = WCBiz.encodePasskeySessionNonce(Numeric.hexStringToByteArray(nonce))
234-
assertEquals(
235-
"0x00000000000000000000000000000000050041d6a66939a8000000000000007b",
236-
Numeric.toHexString(passkeyNonce)
237-
)
238-
}
239-
240-
@Test
241-
fun testSignUserOperationV7WithPasskeySession() {
242-
val chainIdByteArray = "0x7A69".toHexByteArray() // 31337
243-
val wallet = "0x336Cd992a83242D91f556C1F7e855AcA366193e0"
244-
val bizPasskeySessionAccount = "0xa0Cb889707d426A7A386870A03bc70d1b0697598"
245-
val bizPasskeySessionCodeName = "PasskeySession"
246-
val codeVersion = "v1.0.0"
247-
// keccak("PasskeySession(bytes32 userOpHash)")
248-
val typeHash = "0x3463fe66e4d03af5b942aebde2b191eff52d291c0a2c8cc302d786854f34bfc9"
249-
// keccak("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)")
250-
val domainSeparatorHash = "0xd87cd6ef79d4e2b95e15ce8abf732db51ec771f1ca2edccf22a46c729ac56472"
251-
252-
// Step 1. Generate UserOperation and its hash.
253-
254-
val batch = Ethereum.Transaction.SCWalletBatch.newBuilder()
255-
.setWalletType(Ethereum.SCWalletType.Biz4337)
256-
.addCalls(Ethereum.Transaction.SCWalletBatch.BatchedCall.newBuilder().apply {
257-
address = "0x0000000000000000000000000000000000000001"
258-
amount = ByteString.copyFrom("0xde0b6b3a7640000".toHexByteArray())
259-
payload = ByteString.EMPTY
260-
})
261-
.addCalls(Ethereum.Transaction.SCWalletBatch.BatchedCall.newBuilder().apply {
262-
address = "0x0000000000000000000000000000000000000002"
263-
amount = ByteString.copyFrom("0xde0b6b3a7640000".toHexByteArray())
264-
payload = ByteString.EMPTY
265-
})
266-
.addCalls(Ethereum.Transaction.SCWalletBatch.BatchedCall.newBuilder().apply {
267-
address = "0x0000000000000000000000000000000000000003"
268-
amount = ByteString.copyFrom("0xde0b6b3a7640000".toHexByteArray())
269-
payload = ByteString.EMPTY
270-
})
271-
.build()
272-
273-
val actualNonce = Numeric.hexStringToByteArray("0x01")
274-
val passkeyNonce = WCBiz.encodePasskeySessionNonce(actualNonce)
275-
276-
val userOpV07 = Ethereum.UserOperationV0_7.newBuilder().apply {
277-
entryPoint = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"
278-
sender = wallet
279-
preVerificationGas = ByteString.copyFrom("0x186a0".toHexByteArray()) // 100000
280-
verificationGasLimit = ByteString.copyFrom("0x186a0".toHexByteArray()) // 100000
281-
paymaster = "0xf62849f9a0b5bf2913b396098f7c7019b51a820a"
282-
paymasterVerificationGasLimit = ByteString.copyFrom("0x1869F".toHexByteArray()) // 99999
283-
paymasterPostOpGasLimit = ByteString.copyFrom("0x15B38".toHexByteArray()) // 88888
284-
paymasterData = ByteString.copyFrom(
285-
"0x00000000000b0000000000002e234dae75c793f67a35089c9d99245e1c58470b00000000000000000000000000000000000000000000000000000000000186a0072f35038bcacc31bcdeda87c1d9857703a26fb70a053f6e87da5a4e7a1e1f3c4b09fbe2dbff98e7a87ebb45a635234f4b79eff3225d07560039c7764291c97e1b"
286-
.toHexByteArray()
287-
)
288-
}.build()
289-
290-
// Create signing input
291-
val signingInput = Ethereum.SigningInput.newBuilder().apply {
292-
// Dummy private key.
293-
privateKey = ByteString.copyFrom("0x03d99692017473e2d631945a812607b23269d85721e0f370b8d3e7d29a874fd2".toHexByteArray())
294-
chainId = ByteString.copyFrom(chainIdByteArray) // 31337
295-
nonce = ByteString.copyFrom(passkeyNonce)
296-
txMode = Ethereum.TransactionMode.UserOp
297-
gasLimit = ByteString.copyFrom("0x186a0".toHexByteArray()) // 100000
298-
maxFeePerGas = ByteString.copyFrom("0x186A0".toHexByteArray()) // 100000
299-
maxInclusionFeePerGas = ByteString.copyFrom("0x186A0".toHexByteArray()) // 100000
300-
toAddress = "0x61061fCAE11fD5461535e134EfF67A98CFFF44E9"
204+
fun testBizSignExecuteWithSignatureCall() {
205+
// Create ERC20 approve function call
206+
val approveFunc = EthereumAbiFunction("approve")
207+
approveFunc.addParamAddress("0xBC472b43BC237f733c78a581078F58A6a89c46Ec".toHexByteArray(), false)
208+
approveFunc.addParamUInt256("0x03e8".toHexByteArray(), false) // 1000
209+
val approvePayload = EthereumAbi.encode(approveFunc)
210+
211+
// Build the ExecuteWithSignatureInput protobuf
212+
val input = wallet.core.jni.proto.Biz.ExecuteWithSignatureInput.newBuilder()
213+
input.apply {
214+
// Add execution
215+
addExecutions(wallet.core.jni.proto.Biz.Execution.newBuilder().apply {
216+
address = "0x4B0F1812e5Df2A09796481Ff14017e6005508003" // TWT token
217+
amount = ByteString.copyFrom("0x00".toHexByteArray())
218+
payload = ByteString.copyFrom(approvePayload)
219+
}.build())
220+
221+
// Private key
222+
privateKey = ByteString.copyFrom("0xefec50f00ef0c09d967f3e363ee96502ce18a1881f6ac22321aa58071d43c66f".toHexByteArray())
223+
224+
// Nonce
225+
nonce = ByteString.copyFrom("0x00".toHexByteArray())
301226

302-
transaction = Ethereum.Transaction.newBuilder().apply {
303-
scwBatch = batch
227+
// Encoding hash params
228+
encodingHashParams = wallet.core.jni.proto.Biz.EncodingHashParams.newBuilder().apply {
229+
chainId = ByteString.copyFrom("0x38".toHexByteArray()) // 56 (BSC)
230+
codeAddress = "0xba083F0EeAF806603d31582D4e7667fB5A4A1B30"
231+
codeName = "Biz"
232+
codeVersion = "v1.0.0"
233+
typeHash = "0xec429430bbd6d0e373848272230d6fe2bac6319d903762e089c5cae97af53df0"
234+
domainSeparatorHash = "0xd87cd6ef79d4e2b95e15ce8abf732db51ec771f1ca2edccf22a46c729ac56472"
304235
}.build()
236+
}
305237

306-
userOperationV07 = userOpV07
307-
}.build()
308-
309-
val output = AnySigner.sign(signingInput, CoinType.ETHEREUM, Ethereum.SigningOutput.parser())
310-
311-
val userOpHash = output.preHash.toByteArray()
312-
assertEquals(
313-
"0x7762e85586107f2bca787a9163b71f0584eabd84258a93cca0e896589a193571",
314-
Numeric.toHexString(userOpHash)
315-
)
316-
317-
// Step 2. Encode UserOperation hash in Biz format.
318-
319-
val encodedUserOpHash = WCBiz.getEncodedHash(
320-
chainIdByteArray,
321-
bizPasskeySessionAccount,
322-
bizPasskeySessionCodeName,
323-
codeVersion,
324-
typeHash,
325-
domainSeparatorHash,
326-
wallet,
327-
Numeric.toHexString(userOpHash)
328-
)
329-
assertEquals(
330-
"0x7d130331f16bb3d2bc3d72db02879d0745d4452592e56723de8b27cf1ee006c7",
331-
Numeric.toHexString(encodedUserOpHash)
332-
)
333-
334-
// Step 3. Generate a WebAuthn with the given challenge (encodedUserOpHash) and passkey signature.
335-
336-
val clientJsonFirstPart = "{\"type\":\"webauthn.get\",\"challenge\":\""
337-
val challengeBase64 = Base64.encodeUrl(encodedUserOpHash)
338-
val challengeBase64NoPad = challengeBase64.trimEnd('=')
339-
val clientJsonLastPart = "\",\"origin\":\"https://sign.coinbase.com\",\"crossOrigin\":false}"
340-
val clientJson = clientJsonFirstPart + challengeBase64NoPad + clientJsonLastPart
341-
assertEquals(clientJson, "{\"type\":\"webauthn.get\",\"challenge\":\"fRMDMfFrs9K8PXLbAoedB0XURSWS5Wcj3osnzx7gBsc\",\"origin\":\"https://sign.coinbase.com\",\"crossOrigin\":false}")
342-
343-
// Authenticator data for Chrome Profile touchID signature
344-
val authenticatorData = "0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000"
345-
// Signature computed on a device using passkey.
346-
val derSignature = "0x3045022073f8762dd6fb0eb39aea829525658fca612d1c433db6381c9d63a52ee15a26be022100e091f452b74519a2894a96d142bdd1888ac6513f5dff76e48c0312144ef9b382".toHexByteArray()
347-
348-
val passkeySignature = WebAuthnSolidity.getFormattedSignature(authenticatorData, clientJson, derSignature)
349-
assertEquals(
350-
"0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000017000000000000000000000000000000000000000000000000000000000000000173f8762dd6fb0eb39aea829525658fca612d1c433db6381c9d63a52ee15a26be1f6e0bac48bae65e76b5692ebd422e773220a96e491827a067b6b8aead6971cf000000000000000000000000000000000000000000000000000000000000002549960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008a7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a2266524d444d66467273394b3850584c62416f656442305855525357533557636a336f736e7a783767427363222c226f726967696e223a2268747470733a2f2f7369676e2e636f696e626173652e636f6d222c2263726f73734f726967696e223a66616c73657d00000000000000000000000000000000000000000000",
351-
Numeric.toHexString(passkeySignature)
352-
)
353-
354-
// Step 4. Final step. Biz-specific adjustments.
238+
// Call the native function
239+
val encoded = WCBiz.signExecuteWithSignatureCall(input.build().toByteArray())
355240

356-
// `passkeyIndex` can be gotten by using `Biz.getPasskeySessionIndexForValidation()` view function.
357-
// https://github.com/trustwallet/7702-passkey-session/blob/b5c85a5c9a72c19195d1d60a1c90b3908a6a0371/src/BizPasskeySession.sol#L412
358-
val passkeyIndex: Byte = 0x00
359-
val passkeyIndexAttachedSignature = byteArrayOf(passkeyIndex) + passkeySignature
360-
assertEquals(
361-
"0x00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000017000000000000000000000000000000000000000000000000000000000000000173f8762dd6fb0eb39aea829525658fca612d1c433db6381c9d63a52ee15a26be1f6e0bac48bae65e76b5692ebd422e773220a96e491827a067b6b8aead6971cf000000000000000000000000000000000000000000000000000000000000002549960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008a7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a2266524d444d66467273394b3850584c62416f656442305855525357533557636a336f736e7a783767427363222c226f726967696e223a2268747470733a2f2f7369676e2e636f696e626173652e636f6d222c2263726f73734f726967696e223a66616c73657d00000000000000000000000000000000000000000000",
362-
Numeric.toHexString(passkeyIndexAttachedSignature)
363-
)
241+
// Verify the result
242+
val expected = "0x1d92e4b600000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000004b0f1812e5df2a09796481ff14017e6005508003000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044095ea7b3000000000000000000000000bc472b43bc237f733c78a581078f58a6a89c46ec00000000000000000000000000000000000000000000000000000000000003e8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041db18e3a0150ddef964810e480b25592942a22d0b583f7d5cbb33ef6fb4baa66e753af78e967ee374070e16cf963a6cd7a3adb713e50d553aefbc361c48366a101b00000000000000000000000000000000000000000000000000000000000000"
243+
assertEquals(expected, Numeric.toHexString(encoded))
364244
}
365245
}

0 commit comments

Comments
 (0)