Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wallet W5 contract #12

Merged
merged 4 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: 1 addition & 1 deletion Source/TonSwift/Contracts/SendMode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public struct SendMode {

/// Flags to send all available Toncoins
public static func sendMaxTon() -> Self {
return SendMode(payMsgFees: false, ignoreErrors: false, value: .sendRemainingBalance)
return SendMode(payMsgFees: false, ignoreErrors: true, value: .sendRemainingBalance)
}
}

Expand Down
3 changes: 3 additions & 0 deletions Source/TonSwift/Util/OpCodes.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
public enum OpCodes {
public static var OUT_ACTION_SEND_MSG_TAG: Int32 = 0x0ec3c86d
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: 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())

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)
}
}
68 changes: 68 additions & 0 deletions Tests/TonSwiftTests/Wallets/WalletContractV5Test.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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))

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
)
}
}
Loading