From abaf2bb6debbcb5fad409b08f7ec7eee1530433d Mon Sep 17 00:00:00 2001 From: Max Voloshinskii Date: Thu, 18 Jul 2024 14:24:24 +0200 Subject: [PATCH] Add v5r1 contract --- Source/TonSwift/Wallets/WalletV5.swift | 43 ++++-- Source/TonSwift/Wallets/WalletV5Beta.swift | 135 ++++++++++++++++++ .../Wallets/WalletContractV5Test.swift | 38 ++++- 3 files changed, 196 insertions(+), 20 deletions(-) create mode 100644 Source/TonSwift/Wallets/WalletV5Beta.swift diff --git a/Source/TonSwift/Wallets/WalletV5.swift b/Source/TonSwift/Wallets/WalletV5.swift index 65a4d1f..b130d50 100644 --- a/Source/TonSwift/Wallets/WalletV5.swift +++ b/Source/TonSwift/Wallets/WalletV5.swift @@ -4,17 +4,17 @@ import TweetNacl public struct WalletId { public let walletVersion: Int8 = 0 - public let subwalletNumber: Int32 = 0 + public let subwalletNumber: Int32 public let networkGlobalId: Int32 public let workchain: Int8 - - public init(networkGlobalId: Int32, workchain: Int8) { + + public init(networkGlobalId: Int32, workchain: Int8 = 0, subwalletNumber: Int32 = 0) { self.networkGlobalId = networkGlobalId self.workchain = workchain + self.subwalletNumber = subwalletNumber } } -/// WARNING: WalletW5 contract is still in beta. use at your own risk public class WalletV5R1: WalletV5 { public init(seqno: Int64 = 0, workchain: Int8 = 0, @@ -22,7 +22,9 @@ public class WalletV5R1: WalletV5 { walletId: WalletId, plugins: Set
= [] ) { - let code = try! Cell.fromBase64(src: "te6cckEBAQEAIwAIQgLkzzsvTG1qYeoPK1RH0mZ4WyavNjfbLe7mvNGqgm80Eg3NjhE=" + + // https://github.com/ton-blockchain/wallet-contract-v5/blob/4fab977f4fae3a37c1aac216ed2b7e611a9bc2af/build/wallet_v5.compiled.json + let code = try! Cell.fromBase64(src: "te6cckECFAEAAoEAART/APSkE/S88sgLAQIBIAINAgFIAwQC3NAg10nBIJFbj2Mg1wsfIIIQZXh0br0hghBzaW50vbCSXwPgghBleHRuuo60gCDXIQHQdNch+kAw+kT4KPpEMFi9kVvg7UTQgQFB1yH0BYMH9A5voTGRMOGAQNchcH/bPOAxINdJgQKAuZEw4HDiEA8CASAFDAIBIAYJAgFuBwgAGa3OdqJoQCDrkOuF/8AAGa8d9qJoQBDrkOuFj8ACAUgKCwAXsyX7UTQcdch1wsfgABGyYvtRNDXCgCAAGb5fD2omhAgKDrkPoCwBAvIOAR4g1wsfghBzaWduuvLgin8PAeaO8O2i7fshgwjXIgKDCNcjIIAg1yHTH9Mf0x/tRNDSANMfINMf0//XCgAK+QFAzPkQmiiUXwrbMeHywIffArNQB7Dy0IRRJbry4IVQNrry4Ib4I7vy0IgikvgA3gGkf8jKAMsfAc8Wye1UIJL4D95w2zzYEAP27aLt+wL0BCFukmwhjkwCIdc5MHCUIccAs44tAdcoIHYeQ2wg10nACPLgkyDXSsAC8uCTINcdBscSwgBSMLDy0InXTNc5MAGk6GwShAe78uCT10rAAPLgk+1V4tIAAcAAkVvg69csCBQgkXCWAdcsCBwS4lIQseMPINdKERITAJYB+kAB+kT4KPpEMFi68uCR7UTQgQFB1xj0BQSdf8jKAEAEgwf0U/Lgi44UA4MH9Fvy4Iwi1woAIW4Bs7Dy0JDiyFADzxYS9ADJ7VQAcjDXLAgkji0h8uCS0gDtRNDSAFETuvLQj1RQMJExnAGBAUDXIdcKAPLgjuLIygBYzxbJ7VST8sCN4gAQk1vbMeHXTNC01sNe" ) super.init(code:code, seqno: seqno, workchain: workchain, publicKey: publicKey, walletId: walletId, plugins: plugins) } @@ -54,17 +56,25 @@ public class WalletV5: WalletContract { self.plugins = plugins } + // TODO: support minimized version func storeWalletId() -> Builder { - return try! Builder() - .store(int: self.walletId.networkGlobalId, bits: 32) + let context = try! Builder() + .store(bit: true) .store(int: self.walletId.workchain, bits: 8) .store(uint: self.walletId.walletVersion, bits: 8) - .store(uint: self.walletId.subwalletNumber, bits: 32) + .store(uint: self.walletId.subwalletNumber, bits: 15) + .endCell() + .beginParse() + .loadInt(bits: 32) + + return try! Builder() + .store(int: self.walletId.networkGlobalId ^ Int32(context), bits: 32) } public var stateInit: StateInit { let data = try! Builder() - .store(uint: 0, bits: 33) // initial seqno = 0 + .store(bit: true) // is signature auth allowed + .store(uint: 0, bits: 32) // initial seqno .store(self.storeWalletId()) .store(data: publicKey) .store(bit: 0) @@ -98,8 +108,8 @@ public class WalletV5: WalletContract { private func storeOutListExtended(messages: [MessageRelaxed], sendMode: UInt64) throws -> Builder { try Builder() + .storeMaybe(ref: self.storeOutList(messages: messages, sendMode: sendMode)) .store(uint: 0, bits: 1) - .store(ref: self.storeOutList(messages: messages, sendMode: sendMode)) } public func createTransfer(args: WalletTransferData, messageType: MessageType = .ext) throws -> WalletTransfer { @@ -111,9 +121,16 @@ public class WalletV5: WalletContract { .store(uint: messageType.opCode, bits: 32) .store(self.storeWalletId()) - let defaultTimeout = UInt64(Date().timeIntervalSince1970) + 60 // Default timeout: 60 seconds - try signingMessage.store(uint: args.timeout ?? defaultTimeout, bits: 32) - + if (args.seqno == 0) { + // 32 bits with 1 + for _ in 0..<4 { + try signingMessage.store(uint: 0xFFFFFFFF, bits: 8) + } + } else { + let defaultTimeout = UInt64(Date().timeIntervalSince1970) + 60 // Default timeout: 60 seconds + try signingMessage.store(uint: args.timeout ?? defaultTimeout, bits: 32) + } + try signingMessage .store(uint: args.seqno, bits: 32) .store( diff --git a/Source/TonSwift/Wallets/WalletV5Beta.swift b/Source/TonSwift/Wallets/WalletV5Beta.swift new file mode 100644 index 0000000..ff20692 --- /dev/null +++ b/Source/TonSwift/Wallets/WalletV5Beta.swift @@ -0,0 +1,135 @@ +import Foundation +import BigInt +import TweetNacl + +public struct WalletIdBeta { + public let walletVersion: Int8 = 0 + public let subwalletNumber: Int32 = 0 + public let networkGlobalId: Int32 + public let workchain: Int8 + + public init(networkGlobalId: Int32, workchain: Int8) { + self.networkGlobalId = networkGlobalId + self.workchain = workchain + } +} + +/// WARNING: WalletW5 contract is still in beta. use at your own risk +public class WalletV5Beta: WalletV5BetaContract { + public init(seqno: Int64 = 0, + workchain: Int8 = 0, + publicKey: Data, + walletId: WalletIdBeta, + plugins: Set
= [] + ) { + let code = try! Cell.fromBase64(src: "te6cckEBAQEAIwAIQgLkzzsvTG1qYeoPK1RH0mZ4WyavNjfbLe7mvNGqgm80Eg3NjhE=" + ) + super.init(code:code, seqno: seqno, workchain: workchain, publicKey: publicKey, walletId: walletId, plugins: plugins) + } +} + +/// Internal WalletV5 implementation. Use specific revision `WalletV5R1` instead. +public class WalletV5BetaContract: WalletContract { + public let seqno: Int64 + public let workchain: Int8 + public let publicKey: Data + public let walletId: WalletIdBeta + public let plugins: Set
+ public let code: Cell + + fileprivate init(code: Cell, + seqno: Int64 = 0, + workchain: Int8 = 0, + publicKey: Data, + walletId: WalletIdBeta, + plugins: Set
= [] + ) { + self.code = code + self.seqno = seqno + self.workchain = workchain + self.publicKey = publicKey + + self.walletId = walletId + + self.plugins = plugins + } + + func storeWalletId() -> Builder { + return try! Builder() + .store(int: self.walletId.networkGlobalId, bits: 32) + .store(int: self.walletId.workchain, bits: 8) + .store(uint: self.walletId.walletVersion, bits: 8) + .store(uint: self.walletId.subwalletNumber, bits: 32) + } + + public var stateInit: StateInit { + let data = try! Builder() + .store(uint: 0, bits: 33) // initial seqno = 0 + .store(self.storeWalletId()) + .store(data: publicKey) + .store(bit: 0) + .endCell() + + return StateInit(code: self.code, data: data) + } + + func pluginsCompact() -> Set { + Set(self.plugins.map{ a in CompactAddress(a) }) + } + + /* + out_list_empty$_ = OutList 0; + out_list$_ {n:#} prev:^(OutList n) action:OutAction + = OutList (n + 1); + */ + private func storeOutList(messages: [MessageRelaxed], sendMode: UInt64) throws -> Builder { + + var latestCell = Builder() + for message in messages { + latestCell = try Builder() + .store(uint: OpCodes.OUT_ACTION_SEND_MSG_TAG, bits: 32) + .store(uint: sendMode, bits: 8) + .store(ref: latestCell) + .store(ref: try Builder().store(message)) + } + + return latestCell + } + + private func storeOutListExtended(messages: [MessageRelaxed], sendMode: UInt64) throws -> Builder { + try Builder() + .store(uint: 0, bits: 1) + .store(ref: self.storeOutList(messages: messages, sendMode: sendMode)) + } + + public func createTransfer(args: WalletTransferData, messageType: MessageType = .ext) throws -> WalletTransfer { + guard args.messages.count <= 255 else { + throw TonError.custom("Maximum number of messages in a single transfer is 255") + } + + let signingMessage = try Builder() + .store(uint: messageType.opCode, bits: 32) + .store(self.storeWalletId()) + + if (args.seqno == 0) { + // 32 bits with 1 + for _ in 0..<4 { + try signingMessage.store(uint: 0xFFFFFFFF, bits: 8) + } + } else { + let defaultTimeout = UInt64(Date().timeIntervalSince1970) + 60 // Default timeout: 60 seconds + try signingMessage.store(uint: args.timeout ?? defaultTimeout, bits: 32) + } + + try signingMessage + .store(uint: args.seqno, bits: 32) + .store( + self.storeOutListExtended( + messages: args.messages, + sendMode: UInt64(args.sendMode.rawValue) + ) + ) + + return WalletTransfer(signingMessage: signingMessage, signaturePosition: .tail) + } +} diff --git a/Tests/TonSwiftTests/Wallets/WalletContractV5Test.swift b/Tests/TonSwiftTests/Wallets/WalletContractV5Test.swift index 0dadbbd..657fcc4 100644 --- a/Tests/TonSwiftTests/Wallets/WalletContractV5Test.swift +++ b/Tests/TonSwiftTests/Wallets/WalletContractV5Test.swift @@ -8,14 +8,14 @@ final class WalletContractV5Test: XCTestCase { private let publicKey = Data(hex: "5754865e86d0ade1199301bbb0319a25ed6b129c4b0a57f28f62449b3df9c522")! private let secretKey = Data(hex: "34aebb9ea454967f16c407c0f8877763e86212116468169d93a3dcbcafe530c95754865e86d0ade1199301bbb0319a25ed6b129c4b0a57f28f62449b3df9c522")! - func testR1() throws { - let contractR1 = WalletV5R1(workchain: 0, publicKey: publicKey, walletId: WalletId(networkGlobalId: -239, workchain: 0)) + func testBeta() throws { + let contractBeta = WalletV5Beta(workchain: 0, publicKey: publicKey, walletId: WalletIdBeta(networkGlobalId: -239, workchain: 0)) - XCTAssertEqual(try contractR1.address(), try Address.parse("UQCRix440npsvDU88REZ8uUJ4jedPEiX_QlCgi954nhZUrBP")) - XCTAssertEqual(try contractR1.stateInit.data?.toString(), "x{000000007FFFFF888000000000002BAA432F436856F08CC980DDD818CD12F6B5894E25852BF947B1224D9EFCE2912_}") - XCTAssertEqual(try contractR1.stateInit.code?.toString(), "x{02E4CF3B2F4C6D6A61EA0F2B5447D266785B26AF3637DB2DEEE6BCD1AA826F3412}") + XCTAssertEqual(try contractBeta.address(), try Address.parse("UQCRix440npsvDU88REZ8uUJ4jedPEiX_QlCgi954nhZUrBP")) + XCTAssertEqual(try contractBeta.stateInit.data?.toString(), "x{000000007FFFFF888000000000002BAA432F436856F08CC980DDD818CD12F6B5894E25852BF947B1224D9EFCE2912_}") + XCTAssertEqual(try contractBeta.stateInit.code?.toString(), "x{02E4CF3B2F4C6D6A61EA0F2B5447D266785B26AF3637DB2DEEE6BCD1AA826F3412}") - let transferMultiple = try contractR1.createTransfer(args: try argsMultiple()) + let transferMultiple = try contractBeta.createTransfer(args: try argsMultiple()) let signedDataMultiple = try transferMultiple.signMessage(signer: WalletTransferSecretKeySigner(secretKey: secretKey)) let cellMultiple = try Cell(data: signedDataMultiple) @@ -23,7 +23,7 @@ final class WalletContractV5Test: XCTestCase { x{C7E0C94840B0F79FB4A63883F1EB89C1B6D7C28A9FDFFF00614E768FC4445CFA06BA291D85B1C755BFD1C2585EAB9A3FEEEB8AAB3E09BD69940DDCEB2B4FBF04} """) - let transferSingle = try contractR1.createTransfer(args: try argsSingle()) + let transferSingle = try contractBeta.createTransfer(args: try argsSingle()) let signedDataSingle = try transferSingle.signMessage(signer: WalletTransferSecretKeySigner(secretKey: secretKey)) let cellSingle = try Cell(data: signedDataSingle) @@ -32,6 +32,30 @@ final class WalletContractV5Test: XCTestCase { """) } + func testR1() throws { + let contractR1 = WalletV5R1(workchain: 0, publicKey: publicKey, walletId: WalletId(networkGlobalId: -239, workchain: 0)) + + XCTAssertEqual(try contractR1.address(), try Address.parse("UQBiUbwjoB56b7CYtoiPnY5vPh2Fwjva6jEPBhqnttjQKpce")) + XCTAssertEqual(try contractR1.stateInit.data?.toString(), "x{800000003FFFFF88ABAA432F436856F08CC980DDD818CD12F6B5894E25852BF947B1224D9EFCE2912_}") + XCTAssertEqual(try contractR1.stateInit.code?.toString(), "x{FF00F4A413F4BCF2C80B}\n x{2_}\n x{4}\n x{D020D749C120915B8F6320D70B1F2082106578746EBD21821073696E74BDB0925F03E082106578746EBA8EB48020D72101D074D721FA4030FA44F828FA443058BD915BE0ED44D0810141D721F4058307F40E6FA1319130E18040D721707FDB3CE03120D749810280B99130E070E2}\n x{EDA2EDFB02F404216E926C218E4C0221D73930709421C700B38E2D01D72820761E436C20D749C008F2E09320D74AC002F2E09320D71D06C712C2005230B0F2D089D74CD7393001A4E86C128407BBF2E093D74AC000F2E093ED55E2D20001C000915BE0EBD72C08142091709601D72C081C12E25210B1E30F20D74A}\n x{01FA4001FA44F828FA443058BAF2E091ED44D0810141D718F405049D7FC8CA0040048307F453F2E08B8E14038307F45BF2E08C22D70A00216E01B3B0F2D090E2C85003CF1612F400C9ED54}\n x{30D72C08248E2D21F2E092D200ED44D0D2005113BAF2D08F54503091319C01810140D721D70A00F2E08EE2C8CA0058CF16C9ED5493F2C08DE2}\n x{935BDB31E1D74CD0}\n x{8EF0EDA2EDFB218308D722028308D723208020D721D31FD31FD31FED44D0D200D31F20D31FD3FFD70A000AF90140CCF9109A28945F0ADB31E1F2C087DF02B35007B0F2D0845125BAF2E0855036BAF2E086F823BBF2D0882292F800DE01A47FC8CA00CB1F01CF16C9ED542092F80FDE70DB3CD8}\n x{EDA2EDFB02F404216E926C218E4C0221D73930709421C700B38E2D01D72820761E436C20D749C008F2E09320D74AC002F2E09320D71D06C712C2005230B0F2D089D74CD7393001A4E86C128407BBF2E093D74AC000F2E093ED55E2D20001C000915BE0EBD72C08142091709601D72C081C12E25210B1E30F20D74A}\n x{01FA4001FA44F828FA443058BAF2E091ED44D0810141D718F405049D7FC8CA0040048307F453F2E08B8E14038307F45BF2E08C22D70A00216E01B3B0F2D090E2C85003CF1612F400C9ED54}\n x{30D72C08248E2D21F2E092D200ED44D0D2005113BAF2D08F54503091319C01810140D721D70A00F2E08EE2C8CA0058CF16C9ED5493F2C08DE2}\n x{935BDB31E1D74CD0}\n x{2_}\n x{2_}\n x{6E_}\n x{ADCE76A2684020EB90EB85FFC_}\n x{AF1DF6A2684010EB90EB858FC_}\n x{4}\n x{B325FB51341C75C875C2C7E_}\n x{B262FB513435C2802_}\n x{BE5F0F6A2684080A0EB90FA02C_}\n x{F2}\n x{20D70B1F82107369676EBAF2E08A7F}\n x{8EF0EDA2EDFB218308D722028308D723208020D721D31FD31FD31FED44D0D200D31F20D31FD3FFD70A000AF90140CCF9109A28945F0ADB31E1F2C087DF02B35007B0F2D0845125BAF2E0855036BAF2E086F823BBF2D0882292F800DE01A47FC8CA00CB1F01CF16C9ED542092F80FDE70DB3CD8}\n x{EDA2EDFB02F404216E926C218E4C0221D73930709421C700B38E2D01D72820761E436C20D749C008F2E09320D74AC002F2E09320D71D06C712C2005230B0F2D089D74CD7393001A4E86C128407BBF2E093D74AC000F2E093ED55E2D20001C000915BE0EBD72C08142091709601D72C081C12E25210B1E30F20D74A}\n x{01FA4001FA44F828FA443058BAF2E091ED44D0810141D718F405049D7FC8CA0040048307F453F2E08B8E14038307F45BF2E08C22D70A00216E01B3B0F2D090E2C85003CF1612F400C9ED54}\n x{30D72C08248E2D21F2E092D200ED44D0D2005113BAF2D08F54503091319C01810140D721D70A00F2E08EE2C8CA0058CF16C9ED5493F2C08DE2}\n x{935BDB31E1D74CD0}") + + let transferMultiple = try contractR1.createTransfer(args: try argsMultiple()) + let signedDataMultiple = try transferMultiple.signMessage(signer: WalletTransferSecretKeySigner(secretKey: secretKey)) + let cellMultiple = try Cell(data: signedDataMultiple) + + XCTAssertEqual(try cellMultiple.toString(), """ + x{7465AD4E852C611ED9845440DBA85623B8CFEE783149903E61113D4D4777F87B9493977B3687BAEC37F835CCB75DCAF74E49617C01D4260BA86B893BFBBBCF0A} + """) + + let transferSingle = try contractR1.createTransfer(args: try argsSingle()) + let signedDataSingle = try transferSingle.signMessage(signer: WalletTransferSecretKeySigner(secretKey: secretKey)) + let cellSingle = try Cell(data: signedDataSingle) + + XCTAssertEqual(try cellSingle.toString(), """ + x{B2E716ACD0CDDD51437EE4C570971D211601BECA387E57E5678113D61B757CAAA47D3BD246567CF31A10D5940A1D736F94860696FCA6C16F44AA487DE29AE102} + """) + } + private func argsMultiple() throws -> WalletTransferData { return try WalletTransferData( seqno: 2,