Skip to content

Commit 6bae053

Browse files
[Aptos]: Add support for fungible assets (#4206)
* [Aptos]: Add support for fungible assets * Addresses review comments * Minor fix --------- Co-authored-by: Sergei Boiko <[email protected]>
1 parent c764e15 commit 6bae053

File tree

6 files changed

+196
-2
lines changed

6 files changed

+196
-2
lines changed

android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/aptos/TestAptosSigner.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,41 @@ class TestAptosSigner {
197197
"1869b853768f0ba935d67f837a66b172dd39a60ca2315f8d4e0e669bbd35cf2502000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e740e7472616e736665725f636f696e730107e9c192ff55cffab3963c695cff6dbf9dad6aff2bb5ac19a6415cad26a81860d9086d65655f636f696e074d6565436f696e000220b7c7d12080209e9dc14498c80200706e760363fb31782247e82cf57d1d6e5d6c081027000000000000d0070000000000006400000000000000c2276ada0000000001002062e7a6a486553b56a53e89dfae3f780693e537e5b0a7ed33290780e581ca83694030ebd7e95cb464677f411868e2cbfcb22bc01cc63cded36c459dff45e6d2f1354ae4e090e7dfbb509851c0368b343e0e5ecaf6b08e7c1b94c186530b0f7dee0d"
198198
)
199199
}
200+
201+
@Test
202+
fun AptosFungibleAssetTransfer() {
203+
// Successfully broadcasted https://explorer.aptoslabs.com/txn/0x475fc97bcba87907166a720676e1b2f5320e613fd13014df37dcf17b09ff0e98/balanceChange?network=mainnet
204+
val key =
205+
"5d996aa76b3212142792d9130796cd2e11e3c445a93118c08414df4f66bc60ec".toHexBytesInByteString()
206+
207+
val fungibleAssetTransferMessage = Aptos.FungibleAssetTransferMessage.newBuilder()
208+
.setAmount(100000000)
209+
.setTo("0x2d92d71078f11d923c2b703b95a288c0e2ae63c0d29154e6278bf8004f9b4e52")
210+
.setMetadataAddress("0x2ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12")
211+
.build()
212+
val signingInput = Aptos.SigningInput.newBuilder()
213+
.setChainId(1)
214+
.setSender("0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30")
215+
.setSequenceNumber(74)
216+
.setGasUnitPrice(100)
217+
.setMaxGasAmount(20)
218+
.setExpirationTimestampSecs(1736060099)
219+
.setFungibleAssetTransfer(fungibleAssetTransferMessage)
220+
.setPrivateKey(key)
221+
.build()
222+
223+
val result = AnySigner.sign(signingInput, CoinType.APTOS, Aptos.SigningOutput.parser())
224+
assertEquals(
225+
Numeric.cleanHexPrefix(Numeric.toHexString(result.rawTxn.toByteArray())),
226+
"07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f304a00000000000000020000000000000000000000000000000000000000000000000000000000000001167072696d6172795f66756e6769626c655f73746f7265087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010e66756e6769626c655f6173736574084d657461646174610003202ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12202d92d71078f11d923c2b703b95a288c0e2ae63c0d29154e6278bf8004f9b4e520800e1f5050000000014000000000000006400000000000000c32c7a670000000001"
227+
)
228+
assertEquals(
229+
Numeric.cleanHexPrefix(Numeric.toHexString(result.authenticator.signature.toByteArray())),
230+
"2d4c5cbb710b6ef92813597054dbf8d3014529a7d85f6393f01e2a3e978c461c6aa656475b98b453ed3faebf7aa1fdd912bfc59a0c1b6fc44330793994b2e40c"
231+
)
232+
assertEquals(
233+
Numeric.cleanHexPrefix(Numeric.toHexString(result.encoded.toByteArray())),
234+
"07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f304a00000000000000020000000000000000000000000000000000000000000000000000000000000001167072696d6172795f66756e6769626c655f73746f7265087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010e66756e6769626c655f6173736574084d657461646174610003202ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12202d92d71078f11d923c2b703b95a288c0e2ae63c0d29154e6278bf8004f9b4e520800e1f5050000000014000000000000006400000000000000c32c7a6700000000010020ea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c402d4c5cbb710b6ef92813597054dbf8d3014529a7d85f6393f01e2a3e978c461c6aa656475b98b453ed3faebf7aa1fdd912bfc59a0c1b6fc44330793994b2e40c"
235+
)
236+
}
200237
}

rust/chains/tw_aptos/src/aptos_move_packages.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
//
33
// Copyright © 2017 Trust Wallet.
44

5+
use std::str::FromStr;
6+
57
use crate::transaction_payload::{EntryFunction, TransactionPayload};
68
use move_core_types::account_address::AccountAddress;
79
use move_core_types::ident_str;
@@ -205,3 +207,32 @@ pub fn managed_coin_register(coin_type: TypeTag) -> TransactionPayload {
205207
json!([]),
206208
))
207209
}
210+
211+
pub fn fungible_asset_transfer(
212+
metadata_address: AccountAddress,
213+
to: AccountAddress,
214+
amount: u64,
215+
) -> SigningResult<TransactionPayload> {
216+
Ok(TransactionPayload::EntryFunction(EntryFunction::new(
217+
ModuleId::new(
218+
AccountAddress::new([
219+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
220+
0, 0, 0, 1,
221+
]),
222+
ident_str!("primary_fungible_store").to_owned(),
223+
),
224+
ident_str!("transfer").to_owned(),
225+
vec![TypeTag::from_str("0x1::fungible_asset::Metadata")
226+
.tw_err(|_| SigningErrorType::Error_internal)?],
227+
vec![
228+
bcs::encode(&metadata_address)?,
229+
bcs::encode(&to)?,
230+
bcs::encode(&amount)?,
231+
],
232+
json!([
233+
metadata_address.to_hex_literal(),
234+
to.to_hex_literal(),
235+
amount.to_string()
236+
]),
237+
)))
238+
}

rust/chains/tw_aptos/src/transaction_builder.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
use crate::address::from_account_error;
66
use crate::aptos_move_packages::{
77
aptos_account_create_account, aptos_account_transfer, aptos_account_transfer_coins,
8-
coin_transfer, managed_coin_register, token_transfers_cancel_offer_script,
9-
token_transfers_claim_script, token_transfers_offer_script,
8+
coin_transfer, fungible_asset_transfer, managed_coin_register,
9+
token_transfers_cancel_offer_script, token_transfers_claim_script,
10+
token_transfers_offer_script,
1011
};
1112
use crate::constants::{GAS_UNIT_PRICE, MAX_GAS_AMOUNT};
1213
use crate::liquid_staking::{
@@ -147,6 +148,18 @@ impl TransactionFactory {
147148
convert_proto_struct_tag_to_type_tag(func)?,
148149
)
149150
},
151+
OneOftransaction_payload::fungible_asset_transfer(fungible_asset_transfer) => factory
152+
.fungible_asset_transfer(
153+
AccountAddress::from_str(&fungible_asset_transfer.metadata_address)
154+
.map_err(from_account_error)
155+
.into_tw()
156+
.context("Invalid metadata address")?,
157+
AccountAddress::from_str(&fungible_asset_transfer.to)
158+
.map_err(from_account_error)
159+
.into_tw()
160+
.context("Invalid destination address")?,
161+
fungible_asset_transfer.amount,
162+
),
150163
OneOftransaction_payload::None => {
151164
let is_blind_sign = !input.any_encoded.is_empty();
152165
let v = serde_json::from_str::<Value>(&input.any_encoded)
@@ -253,6 +266,15 @@ impl TransactionFactory {
253266
Ok(self.payload(coin_transfer(coin_type, to, amount)?))
254267
}
255268

269+
pub fn fungible_asset_transfer(
270+
&self,
271+
metadata_address: AccountAddress,
272+
to: AccountAddress,
273+
amount: u64,
274+
) -> SigningResult<TransactionBuilder> {
275+
Ok(self.payload(fungible_asset_transfer(metadata_address, to, amount)?))
276+
}
277+
256278
pub fn implicitly_create_user_and_coins_transfer(
257279
&self,
258280
to: AccountAddress,

rust/chains/tw_aptos/tests/signer.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ pub struct TokenTransfer {
2929
tag: TypeTag,
3030
}
3131

32+
pub struct FungibleAssetTransfer {
33+
metadata_address: String,
34+
to: String,
35+
amount: u64,
36+
}
37+
3238
pub struct RegisterToken {
3339
coin_type: TypeTag,
3440
}
@@ -41,6 +47,7 @@ pub enum OpsDetails {
4147
TokenTransfer(TokenTransfer),
4248
ImplicitTokenTransfer(TokenTransfer),
4349
NftOps(NftOperation),
50+
FungibleAssetTransfer(FungibleAssetTransfer),
4451
}
4552

4653
fn setup_proto_transaction<'a>(
@@ -135,6 +142,20 @@ fn setup_proto_transaction<'a>(
135142
panic!("Unsupported arguments")
136143
}
137144
},
145+
"fungible_asset_transfer" => {
146+
if let OpsDetails::FungibleAssetTransfer(fungible_asset_transfer) = ops_details.unwrap()
147+
{
148+
Proto::mod_SigningInput::OneOftransaction_payload::fungible_asset_transfer(
149+
Proto::FungibleAssetTransferMessage {
150+
to: fungible_asset_transfer.to.into(),
151+
amount: fungible_asset_transfer.amount,
152+
metadata_address: fungible_asset_transfer.metadata_address.into(),
153+
},
154+
)
155+
} else {
156+
panic!("Unsupported arguments")
157+
}
158+
},
138159
"blind_sign_json" => Proto::mod_SigningInput::OneOftransaction_payload::None,
139160
_ => Proto::mod_SigningInput::OneOftransaction_payload::None,
140161
};
@@ -321,6 +342,52 @@ fn test_aptos_sign_coin_transfer() {
321342
}"#);
322343
}
323344

345+
// Successfully broadcasted https://explorer.aptoslabs.com/txn/0x475fc97bcba87907166a720676e1b2f5320e613fd13014df37dcf17b09ff0e98/balanceChange?network=mainnet
346+
#[test]
347+
fn test_aptos_sign_fungible_asset_transfer() {
348+
let input = setup_proto_transaction(
349+
"0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30", // Sender's address
350+
"5d996aa76b3212142792d9130796cd2e11e3c445a93118c08414df4f66bc60ec", // Keypair
351+
"fungible_asset_transfer",
352+
74, // Sequence number
353+
1,
354+
20,
355+
1736060099,
356+
100,
357+
"",
358+
"",
359+
Some(OpsDetails::FungibleAssetTransfer(FungibleAssetTransfer {
360+
metadata_address: "0x2ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12"
361+
.to_string(),
362+
to: "0x2d92d71078f11d923c2b703b95a288c0e2ae63c0d29154e6278bf8004f9b4e52".to_string(),
363+
amount: 100000000,
364+
})),
365+
);
366+
let output = Signer::sign_proto(input);
367+
test_tx_result(output,
368+
"07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f304a00000000000000020000000000000000000000000000000000000000000000000000000000000001167072696d6172795f66756e6769626c655f73746f7265087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010e66756e6769626c655f6173736574084d657461646174610003202ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12202d92d71078f11d923c2b703b95a288c0e2ae63c0d29154e6278bf8004f9b4e520800e1f5050000000014000000000000006400000000000000c32c7a670000000001", // Expected raw transaction bytes
369+
"2d4c5cbb710b6ef92813597054dbf8d3014529a7d85f6393f01e2a3e978c461c6aa656475b98b453ed3faebf7aa1fdd912bfc59a0c1b6fc44330793994b2e40c", // Expected signature
370+
"07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f304a00000000000000020000000000000000000000000000000000000000000000000000000000000001167072696d6172795f66756e6769626c655f73746f7265087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010e66756e6769626c655f6173736574084d657461646174610003202ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12202d92d71078f11d923c2b703b95a288c0e2ae63c0d29154e6278bf8004f9b4e520800e1f5050000000014000000000000006400000000000000c32c7a6700000000010020ea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c402d4c5cbb710b6ef92813597054dbf8d3014529a7d85f6393f01e2a3e978c461c6aa656475b98b453ed3faebf7aa1fdd912bfc59a0c1b6fc44330793994b2e40c", // Expected encoded transaction
371+
r#"{
372+
"expiration_timestamp_secs": "1736060099",
373+
"gas_unit_price": "100",
374+
"max_gas_amount": "20",
375+
"payload": {
376+
"arguments": ["0x2ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12","0x2d92d71078f11d923c2b703b95a288c0e2ae63c0d29154e6278bf8004f9b4e52", "100000000"],
377+
"function": "0x1::primary_fungible_store::transfer",
378+
"type": "entry_function_payload",
379+
"type_arguments": ["0x1::fungible_asset::Metadata"]
380+
},
381+
"sender": "0x7968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30",
382+
"sequence_number": "74",
383+
"signature": {
384+
"public_key": "0xea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c",
385+
"signature": "0x2d4c5cbb710b6ef92813597054dbf8d3014529a7d85f6393f01e2a3e978c461c6aa656475b98b453ed3faebf7aa1fdd912bfc59a0c1b6fc44330793994b2e40c",
386+
"type": "ed25519_signature"
387+
}
388+
}"#);
389+
}
390+
324391
// Successfully broadcasted https://explorer.aptoslabs.com/txn/0x197d40ea12e2bfc65a0a913b9f4ca3b0b0208fe0c1514d3d55cef3d5bcf25211?network=mainnet
325392
#[test]
326393
fn test_implicit_aptos_sign_coin_transfer() {

src/proto/Aptos.proto

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ message TokenTransferCoinsMessage {
4848
StructTag function = 3;
4949
}
5050

51+
message FungibleAssetTransferMessage {
52+
// Fungible Asset address (string)
53+
string metadata_address = 1;
54+
// Destination Account address (string)
55+
string to = 2;
56+
// Amount to be transferred (uint64)
57+
uint64 amount = 3;
58+
}
59+
5160
// Necessary fields to process a ManagedTokensRegisterMessage
5261
message ManagedTokensRegisterMessage {
5362
// token function to register, e.g BTC: 0x43417434fd869edee76cca2a4d2301e528a1551b1d719b75c350c3c97d15b8b9::coins::BTC
@@ -165,6 +174,7 @@ message SigningInput {
165174
ManagedTokensRegisterMessage register_token = 13;
166175
LiquidStaking liquid_staking_message = 14;
167176
TokenTransferCoinsMessage token_transfer_coins = 15;
177+
FungibleAssetTransferMessage fungible_asset_transfer = 16;
168178
}
169179

170180
string abi = 21;

swift/Tests/Blockchains/AptosTests.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,31 @@ class AptosTests: XCTestCase {
122122
XCTAssertEqual(output.authenticator.signature.hexString, expectedSignature)
123123
XCTAssertEqual(output.encoded.hexString, expectedSignedTx)
124124
}
125+
126+
func testSignFungibleAssetTransfer() {
127+
// Successfully broadcasted https://explorer.aptoslabs.com/txn/0x475fc97bcba87907166a720676e1b2f5320e613fd13014df37dcf17b09ff0e98/balanceChange?network=mainnet
128+
let privateKeyData = Data(hexString: "5d996aa76b3212142792d9130796cd2e11e3c445a93118c08414df4f66bc60ec")!
129+
let fungibleAssetTransferMsg = AptosFungibleAssetTransferMessage.with {
130+
$0.metadataAddress = "0x2ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12"
131+
$0.to = "0x2d92d71078f11d923c2b703b95a288c0e2ae63c0d29154e6278bf8004f9b4e52"
132+
$0.amount = 100000000
133+
}
134+
let input = AptosSigningInput.with {
135+
$0.chainID = 1
136+
$0.sender = "0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30"
137+
$0.expirationTimestampSecs = 1736060099
138+
$0.gasUnitPrice = 100
139+
$0.maxGasAmount = 20
140+
$0.sequenceNumber = 74
141+
$0.fungibleAssetTransfer = fungibleAssetTransferMsg
142+
$0.privateKey = privateKeyData
143+
}
144+
let output: AptosSigningOutput = AnySigner.sign(input: input, coin: .aptos)
145+
let expectedRawTx = "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f304a00000000000000020000000000000000000000000000000000000000000000000000000000000001167072696d6172795f66756e6769626c655f73746f7265087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010e66756e6769626c655f6173736574084d657461646174610003202ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12202d92d71078f11d923c2b703b95a288c0e2ae63c0d29154e6278bf8004f9b4e520800e1f5050000000014000000000000006400000000000000c32c7a670000000001"
146+
let expectedSignature = "2d4c5cbb710b6ef92813597054dbf8d3014529a7d85f6393f01e2a3e978c461c6aa656475b98b453ed3faebf7aa1fdd912bfc59a0c1b6fc44330793994b2e40c"
147+
let expectedSignedTx = "07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f304a00000000000000020000000000000000000000000000000000000000000000000000000000000001167072696d6172795f66756e6769626c655f73746f7265087472616e73666572010700000000000000000000000000000000000000000000000000000000000000010e66756e6769626c655f6173736574084d657461646174610003202ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12202d92d71078f11d923c2b703b95a288c0e2ae63c0d29154e6278bf8004f9b4e520800e1f5050000000014000000000000006400000000000000c32c7a6700000000010020ea526ba1710343d953461ff68641f1b7df5f23b9042ffa2d2a798d3adb3f3d6c402d4c5cbb710b6ef92813597054dbf8d3014529a7d85f6393f01e2a3e978c461c6aa656475b98b453ed3faebf7aa1fdd912bfc59a0c1b6fc44330793994b2e40c"
148+
XCTAssertEqual(output.rawTxn.hexString, expectedRawTx)
149+
XCTAssertEqual(output.authenticator.signature.hexString, expectedSignature)
150+
XCTAssertEqual(output.encoded.hexString, expectedSignedTx)
151+
}
125152
}

0 commit comments

Comments
 (0)