Skip to content

Commit

Permalink
Merge pull request #13 from tonkeeper/feature/v5r1-wallet
Browse files Browse the repository at this point in the history
Add v5r1 contract
  • Loading branch information
grishamsc authored Jul 18, 2024
2 parents bda58e8 + abaf2bb commit 2f8737e
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 20 deletions.
43 changes: 30 additions & 13 deletions Source/TonSwift/Wallets/WalletV5.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,27 @@ 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,
publicKey: Data,
walletId: WalletId,
plugins: Set<Address> = []
) {
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)
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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(
Expand Down
135 changes: 135 additions & 0 deletions Source/TonSwift/Wallets/WalletV5Beta.swift
Original file line number Diff line number Diff line change
@@ -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<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 WalletV5BetaContract: WalletContract {
public let seqno: Int64
public let workchain: Int8
public let publicKey: Data
public let walletId: WalletIdBeta
public let plugins: Set<Address>
public let code: Cell

fileprivate init(code: Cell,
seqno: Int64 = 0,
workchain: Int8 = 0,
publicKey: Data,
walletId: WalletIdBeta,
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())

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)
}
}
38 changes: 31 additions & 7 deletions Tests/TonSwiftTests/Wallets/WalletContractV5Test.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,22 @@ 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)

XCTAssertEqual(try cellMultiple.toString(), """
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)

Expand All @@ -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,
Expand Down

0 comments on commit 2f8737e

Please sign in to comment.