Skip to content

Commit

Permalink
Wallet W5 contract
Browse files Browse the repository at this point in the history
  • Loading branch information
voloshinskii committed Jun 13, 2024
1 parent f967c55 commit 7058e22
Show file tree
Hide file tree
Showing 10 changed files with 244 additions and 20 deletions.
5 changes: 3 additions & 2 deletions Source/TonSwift/Cells/Cell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
7 changes: 6 additions & 1 deletion Source/TonSwift/Contracts/MessageRelaxed.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
2 changes: 2 additions & 0 deletions Source/TonSwift/Util/OpCodes.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down
22 changes: 20 additions & 2 deletions Source/TonSwift/Wallets/WalletContract.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions Source/TonSwift/Wallets/WalletV1.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ 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 {
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)
}
}
4 changes: 2 additions & 2 deletions Source/TonSwift/Wallets/WalletV2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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)
}
}
6 changes: 3 additions & 3 deletions Source/TonSwift/Wallets/WalletV3.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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)
}
}
16 changes: 8 additions & 8 deletions Source/TonSwift/Wallets/WalletV4.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ public class WalletV4: WalletContract {
public let walletId: UInt32
public let plugins: Set<Address>
public let code: Cell

fileprivate init(code: Cell,
seqno: Int64 = 0,
workchain: Int8 = 0,
publicKey: Data,
walletId: UInt32? = nil,
plugins: Set<Address> = []
seqno: Int64 = 0,
workchain: Int8 = 0,
publicKey: Data,
walletId: UInt32? = nil,
plugins: Set<Address> = []
) {
self.code = code
self.seqno = seqno
Expand Down Expand Up @@ -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")
}
Expand All @@ -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)
}
}
128 changes: 128 additions & 0 deletions Source/TonSwift/Wallets/WalletV5.swift
Original file line number Diff line number Diff line change
@@ -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<Address> = []
) {
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<Address>
public let code: Cell

fileprivate init(code: Cell,
seqno: Int64 = 0,
workchain: Int8 = 0,
publicKey: Data,
walletId: WalletId,
plugins: Set<Address> = []
) {
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<CompactAddress> {
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)
}
}
70 changes: 70 additions & 0 deletions Tests/TonSwiftTests/Wallets/WalletContractV5Test.swift
Original file line number Diff line number Diff line change
@@ -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
)
}
}

0 comments on commit 7058e22

Please sign in to comment.