From 7058e22b42ebc97f1df2a79e72a05ddb71ed784b Mon Sep 17 00:00:00 2001 From: Max Voloshinskii Date: Thu, 13 Jun 2024 08:33:38 +0300 Subject: [PATCH] Wallet W5 contract --- Source/TonSwift/Cells/Cell.swift | 5 +- .../TonSwift/Contracts/MessageRelaxed.swift | 7 +- Source/TonSwift/Util/OpCodes.swift | 2 + Source/TonSwift/Wallets/WalletContract.swift | 22 ++- Source/TonSwift/Wallets/WalletV1.swift | 4 +- Source/TonSwift/Wallets/WalletV2.swift | 4 +- Source/TonSwift/Wallets/WalletV3.swift | 6 +- Source/TonSwift/Wallets/WalletV4.swift | 16 +-- Source/TonSwift/Wallets/WalletV5.swift | 128 ++++++++++++++++++ .../Wallets/WalletContractV5Test.swift | 70 ++++++++++ 10 files changed, 244 insertions(+), 20 deletions(-) create mode 100644 Source/TonSwift/Wallets/WalletV5.swift create mode 100644 Tests/TonSwiftTests/Wallets/WalletContractV5Test.swift diff --git a/Source/TonSwift/Cells/Cell.swift b/Source/TonSwift/Cells/Cell.swift index f11e6d1..a39b77a 100644 --- a/Source/TonSwift/Cells/Cell.swift +++ b/Source/TonSwift/Cells/Cell.swift @@ -10,6 +10,7 @@ public let RefsPerCell = 4 public enum CellType: Int { case ordinary = -1 case prunedBranch = 1 + case library = 2 case merkleProof = 3 case merkleUpdate = 4 } @@ -224,7 +225,7 @@ fileprivate struct BasicCell: Hashable { type = try resolvePruned(bits: bits, refs: refs).type case 2: - throw TonError.custom("Library cell must be loaded automatically") + type = .library case 3: type = try resolveMerkleProof(bits: bits, refs: refs).type @@ -246,7 +247,7 @@ fileprivate struct BasicCell: Hashable { var pruned: ExoticPruned? = nil switch type { - case .ordinary: + case .ordinary, .library: var mask: UInt32 = 0 for r in refs { mask = mask | r.mask.value diff --git a/Source/TonSwift/Contracts/MessageRelaxed.swift b/Source/TonSwift/Contracts/MessageRelaxed.swift index a5eaee6..1d3ab92 100644 --- a/Source/TonSwift/Contracts/MessageRelaxed.swift +++ b/Source/TonSwift/Contracts/MessageRelaxed.swift @@ -85,7 +85,12 @@ public struct MessageRelaxed: CellCodable { ) } public static func `internal`(to: Address, value: BigUInt, bounce: Bool = true, stateInit: StateInit? = nil, textPayload: String) throws -> MessageRelaxed { - let body = try Builder().store(int: 0, bits: 32).writeSnakeData(Data(textPayload.utf8)).endCell() + let body: Cell + if (textPayload.isEmpty) { + body = .empty + } else { + body = try Builder().store(int: 0, bits: 32).writeSnakeData(Data(textPayload.utf8)).endCell() + } return .internal(to: to, value: value, bounce: bounce, stateInit: stateInit, body: body) } } diff --git a/Source/TonSwift/Util/OpCodes.swift b/Source/TonSwift/Util/OpCodes.swift index 4195633..1981798 100644 --- a/Source/TonSwift/Util/OpCodes.swift +++ b/Source/TonSwift/Util/OpCodes.swift @@ -1,4 +1,6 @@ public enum OpCodes { + public static var SIGNED_EXTERNAL: Int32 = 0x7369676e + public static var SIGNED_INTERNAL: Int32 = 0x73696e74 public static var JETTON_TRANSFER: Int32 = 0xf8a7ea5 public static var NFT_TRANSFER: Int32 = 0x5fcc3d14 public static var STONFI_SWAP: Int32 = 0x25938561 diff --git a/Source/TonSwift/Wallets/WalletContract.swift b/Source/TonSwift/Wallets/WalletContract.swift index 3d15f32..3e4cc53 100644 --- a/Source/TonSwift/Wallets/WalletContract.swift +++ b/Source/TonSwift/Wallets/WalletContract.swift @@ -3,7 +3,19 @@ import Foundation /// All wallets implement a compatible interface for sending messages public protocol WalletContract: Contract { - func createTransfer(args: WalletTransferData) throws -> WalletTransfer + func createTransfer(args: WalletTransferData, messageType: MessageType) throws -> WalletTransfer +} + +/// Message type (external | internal) to sign. Is using in v5 wallet contract +public enum MessageType { + case int, ext + + var opCode: Int32 { + switch self { + case .int: return OpCodes.SIGNED_INTERNAL + case .ext: return OpCodes.SIGNED_EXTERNAL + } + } } public struct WalletTransferData { @@ -23,11 +35,17 @@ public struct WalletTransferData { } } +public enum SignaturePosition { + case front, tail +} + public struct WalletTransfer { public let signingMessage: Builder + public let signaturePosition: SignaturePosition - public init(signingMessage: Builder) { + public init(signingMessage: Builder, signaturePosition: SignaturePosition) { self.signingMessage = signingMessage + self.signaturePosition = signaturePosition } public func signMessage(signer: WalletTransferSigner) throws -> Data { diff --git a/Source/TonSwift/Wallets/WalletV1.swift b/Source/TonSwift/Wallets/WalletV1.swift index 6430e90..cfd7e0f 100644 --- a/Source/TonSwift/Wallets/WalletV1.swift +++ b/Source/TonSwift/Wallets/WalletV1.swift @@ -36,7 +36,7 @@ public final class WalletV1: WalletContract { self.stateInit = StateInit(code: cell, data: try data.endCell()) } - public func createTransfer(args: WalletTransferData) throws -> WalletTransfer { + public func createTransfer(args: WalletTransferData, messageType: MessageType = .ext) throws -> WalletTransfer { let signingMessage = try Builder().store(uint: args.seqno, bits: 32) if let message = args.messages.first { @@ -44,6 +44,6 @@ public final class WalletV1: WalletContract { try signingMessage.store(ref:try Builder().store(message)) } - return WalletTransfer(signingMessage: signingMessage) + return WalletTransfer(signingMessage: signingMessage, signaturePosition: .front) } } diff --git a/Source/TonSwift/Wallets/WalletV2.swift b/Source/TonSwift/Wallets/WalletV2.swift index ef108c9..2d03bb9 100644 --- a/Source/TonSwift/Wallets/WalletV2.swift +++ b/Source/TonSwift/Wallets/WalletV2.swift @@ -33,7 +33,7 @@ public final class WalletV2: WalletContract { self.stateInit = StateInit(code: cell, data: try data.endCell()) } - public func createTransfer(args: WalletTransferData) throws -> WalletTransfer { + public func createTransfer(args: WalletTransferData, messageType: MessageType = .ext) throws -> WalletTransfer { guard args.messages.count <= 4 else { throw TonError.custom("Maximum number of messages in a single transfer is 4") } @@ -47,6 +47,6 @@ public final class WalletV2: WalletContract { try signingMessage.store(ref:try Builder().store(message)) } - return WalletTransfer(signingMessage: signingMessage) + return WalletTransfer(signingMessage: signingMessage, signaturePosition: .front) } } diff --git a/Source/TonSwift/Wallets/WalletV3.swift b/Source/TonSwift/Wallets/WalletV3.swift index d86d069..4b5d931 100644 --- a/Source/TonSwift/Wallets/WalletV3.swift +++ b/Source/TonSwift/Wallets/WalletV3.swift @@ -42,7 +42,7 @@ public final class WalletV3: WalletContract { self.stateInit = StateInit(code: cell, data: try data.endCell()) } - public func createTransfer(args: WalletTransferData) throws -> WalletTransfer { + public func createTransfer(args: WalletTransferData, messageType: MessageType = .ext) throws -> WalletTransfer { guard args.messages.count <= 4 else { throw TonError.custom("Maximum number of messages in a single transfer is 4") } @@ -56,7 +56,7 @@ public final class WalletV3: WalletContract { try signingMessage.store(uint: UInt64(args.sendMode.rawValue), bits: 8) try signingMessage.store(ref: try Builder().store(message)) } - - return WalletTransfer(signingMessage: signingMessage) + + return WalletTransfer(signingMessage: signingMessage, signaturePosition: .front) } } diff --git a/Source/TonSwift/Wallets/WalletV4.swift b/Source/TonSwift/Wallets/WalletV4.swift index beb8a98..f12f0b7 100644 --- a/Source/TonSwift/Wallets/WalletV4.swift +++ b/Source/TonSwift/Wallets/WalletV4.swift @@ -34,13 +34,13 @@ public class WalletV4: WalletContract { public let walletId: UInt32 public let plugins: Set
public let code: Cell - + fileprivate init(code: Cell, - seqno: Int64 = 0, - workchain: Int8 = 0, - publicKey: Data, - walletId: UInt32? = nil, - plugins: Set
= [] + seqno: Int64 = 0, + workchain: Int8 = 0, + publicKey: Data, + walletId: UInt32? = nil, + plugins: Set
= [] ) { self.code = code self.seqno = seqno @@ -70,7 +70,7 @@ public class WalletV4: WalletContract { Set(self.plugins.map{ a in CompactAddress(a) }) } - public func createTransfer(args: WalletTransferData) throws -> WalletTransfer { + public func createTransfer(args: WalletTransferData, messageType: MessageType = .ext) throws -> WalletTransfer { guard args.messages.count <= 4 else { throw TonError.custom("Maximum number of messages in a single transfer is 4") } @@ -86,6 +86,6 @@ public class WalletV4: WalletContract { try signingMessage.store(ref: try Builder().store(message)) } - return WalletTransfer(signingMessage: signingMessage) + return WalletTransfer(signingMessage: signingMessage, signaturePosition: .front) } } diff --git a/Source/TonSwift/Wallets/WalletV5.swift b/Source/TonSwift/Wallets/WalletV5.swift new file mode 100644 index 0000000..cb8b534 --- /dev/null +++ b/Source/TonSwift/Wallets/WalletV5.swift @@ -0,0 +1,128 @@ +import Foundation +import BigInt +import TweetNacl + +public struct WalletId { + 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 WalletV5R1: WalletV5 { + public init(seqno: Int64 = 0, + workchain: Int8 = 0, + publicKey: Data, + walletId: WalletId, + 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 WalletV5: WalletContract { + public let seqno: Int64 + public let workchain: Int8 + public let publicKey: Data + public let walletId: WalletId + public let plugins: Set
+ public let code: Cell + + fileprivate init(code: Cell, + seqno: Int64 = 0, + workchain: Int8 = 0, + publicKey: Data, + walletId: WalletId, + 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: 0x0ec3c86d, 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()) + + 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 new file mode 100644 index 0000000..2aa1fcf --- /dev/null +++ b/Tests/TonSwiftTests/Wallets/WalletContractV5Test.swift @@ -0,0 +1,70 @@ +import XCTest +import TweetNacl +import BigInt +@testable import TonSwift + +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)) + + print(try contractR1.address()) + + 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}") + + 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{C7E0C94840B0F79FB4A63883F1EB89C1B6D7C28A9FDFFF00614E768FC4445CFA06BA291D85B1C755BFD1C2585EAB9A3FEEEB8AAB3E09BD69940DDCEB2B4FBF04} + """) + + 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{789A0A8331A8901A042E0717201F285612FF24B8E758222EA1EF69EE645C9B6DE7B17DBA9677CA69CFC6BA89783B3AFA8D5FCB933C0CF1A4532BEC5F87BDCE04} + """) + } + + private func argsMultiple() throws -> WalletTransferData { + return try WalletTransferData( + seqno: 2, + messages: [ + .internal( + to: Address.parse("kQD6oPnzaaAMRW24R8F0_nlSsJQni0cGHntR027eT9_sgtwt"), + value: BigUInt(0.1 * 1000000000), + textPayload: "Hello world: 1" + ), + .internal( + to: Address.parse("kQD6oPnzaaAMRW24R8F0_nlSsJQni0cGHntR027eT9_sgtwt"), + value: BigUInt(0.1 * 1000000000), + textPayload: "Hello world: 2" + ) + ], + sendMode: SendMode(payMsgFees: true), + timeout: 1680179023 + ) + } + + private func argsSingle() throws -> WalletTransferData { + return try WalletTransferData( + seqno: 2, + messages: [ + .internal( + to: Address.parse("kQD6oPnzaaAMRW24R8F0_nlSsJQni0cGHntR027eT9_sgtwt"), + value: BigUInt(1 * 1000000000) + ), + ], + sendMode: SendMode(payMsgFees: true), + timeout: 1680179023 + ) + } +}