Skip to content

Commit

Permalink
Merge pull request #59 from XYOracleNetwork/feature/child-account-der…
Browse files Browse the repository at this point in the history
…ivation

Child Account Derivation
  • Loading branch information
JoelBCarter authored Dec 6, 2024
2 parents b1ae730 + 51076a4 commit 5fe807e
Show file tree
Hide file tree
Showing 36 changed files with 763 additions and 416 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"Alamofire",
"Arie",
"boundwitness",
"hmac",
"keccak",
"Keychain",
"Protobuf",
Expand Down
158 changes: 105 additions & 53 deletions Sources/XyoClient/Address/Account.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import BigInt
import Foundation
import secp256k1

public func dataFromHex(_ hex: String) -> Data? {
let hex = hex.replacingOccurrences(of: " ", with: "") // Remove any spaces
let hex = hex.replacingOccurrences(of: " ", with: "") // Remove any spaces
let len = hex.count

// Ensure even length (hex must be in pairs)
Expand All @@ -21,9 +22,9 @@ public func dataFromHex(_ hex: String) -> Data? {
return data
}

public extension Data {
init?(_ hex: String) {
let hex = hex.replacingOccurrences(of: " ", with: "") // Remove any spaces
extension Data {
public init?(_ hex: String) {
let hex = hex.replacingOccurrences(of: " ", with: "") // Remove any spaces
let len = hex.count

// Ensure even length (hex must be in pairs)
Expand All @@ -38,7 +39,7 @@ public extension Data {
data.append(byte)
index = nextIndex
}

guard let result = dataFromHex(hex) else { return nil }

self = result
Expand All @@ -47,31 +48,73 @@ public extension Data {

enum AccountError: Error {
case invalidAddress
case invalidPrivateKey
case invalidMessage
case invalidPrivateKey
}

public class Account: AccountInstance, AccountStatic {

public static var previousHashStore: PreviousHashStore = CoreDataPreviousHashStore()

private var _privateKey: Data?


public var address: Data? {
// Get the keccak hash of the public key
guard let keccakBytes = keccakBytes else { return nil }
// Return the last 20 bytes of the keccak hash
return keccakBytes.suffix(20)
}

public var keccakBytes: Data? {
return publicKeyUncompressed?
// Drop the `0x04` from the beginning of the key
.dropFirst()
// Then take the keccak256 hash of the key
.keccak256()
}

public var previousHash: Hash? {
return try? retrievePreviousHash()
}

public var privateKey: Data? {
return _privateKey
}

public var publicKey: Data? {
guard let privateKey = self.privateKey else {return nil}
guard
let publicKeyUncompressed = publicKeyUncompressed?
// Drop the `0x04` from the beginning of the key
.dropFirst()
else { return nil }
return try? Account.getCompressedPublicKeyFrom(uncompressedPublicKey: publicKeyUncompressed)
}

public var publicKeyUncompressed: Data? {
guard let privateKey = self.privateKey else { return nil }
return try? Account.privateKeyObjectFromKey(privateKey).publicKey.dataRepresentation
}


public static func fromPrivateKey(_ key: Data) -> any AccountInstance {
return Account(key)
}

public static func fromPrivateKey(_ key: String) throws -> AccountInstance {
guard let data = Data(key) else { throw AccountError.invalidPrivateKey }
return Account(data)
}

public func sign(hash: String) throws -> Signature {
guard let message = hash.hexToData() else { throw AccountError.invalidMessage }
return try self.sign(message)

public static func random() -> AccountInstance {
return Account(generateRandomBytes())
}

init(_ privateKey: Data) {
self._privateKey = privateKey
}

public func sign(_ hash: Hash) throws -> Signature {
let context = try secp256k1.Context.create()
guard let privateKey = self.privateKey else {throw AccountError.invalidPrivateKey}
guard let privateKey = self.privateKey else { throw AccountError.invalidPrivateKey }

defer { secp256k1_context_destroy(context) }

Expand Down Expand Up @@ -100,49 +143,24 @@ public class Account: AccountInstance, AccountStatic {
count: MemoryLayout.size(ofValue: signature2.data)
)

let result = try secp256k1.Signing.ECDSASignature(dataRepresentation: rawRepresentation).dataRepresentation
let result = try secp256k1.Signing.ECDSASignature(dataRepresentation: rawRepresentation)
.dataRepresentation
try self.storePreviousHash(hash)
return result
}

public func verify(_ msg: Data, _ signature: Signature) -> Bool {
return false
}

public var keccakBytes: Data? {
return publicKey?.keccak256()
}

public var address: Data? {
guard let keccakBytes = keccakBytes else {return nil}
return keccakBytes.subdata(in: 12..<keccakBytes.count)
}

public static var previousHashStore: PreviousHashStore = CoreDataPreviousHashStore()

public static func fromPrivateKey(_ key: Data) -> any AccountInstance {
return Account(key)
}

public var privateKey: Data? {
return _privateKey
public func sign(hash: String) throws -> Signature {
guard let message = hash.hexToData() else { throw AccountError.invalidMessage }
return try self.sign(message)
}

public static func random() -> AccountInstance {
return Account(generateRandomBytes())
public func verify(_ msg: Data, _ signature: Signature) -> Bool {
return false
}

init(_ privateKey: Data) {
self._privateKey = privateKey
}

public var previousHash: Hash? {
return try? retreivePreviousHash()
}

public static func privateKeyObjectFromKey(_ key: Data) throws -> secp256k1.Signing.PrivateKey {
return try secp256k1.Signing.PrivateKey(
dataRepresentation: key, format: .uncompressed)
internal func retrievePreviousHash() throws -> Hash? {
guard let address = self.address else { throw AccountError.invalidAddress }
return Account.previousHashStore.getItem(address: address)
}

internal func storePreviousHash(_ newValue: Hash?) throws {
Expand All @@ -151,9 +169,43 @@ public class Account: AccountInstance, AccountStatic {
Account.previousHashStore.setItem(address: address, previousHash: previousHash)
}
}

internal func retreivePreviousHash() throws -> Hash? {
guard let address = self.address else { throw AccountError.invalidAddress }
return Account.previousHashStore.getItem(address: address)

public static func getCompressedPublicKeyFrom(privateKey: Data) throws -> Data {
guard
let uncompressedPublicKey = XyoAddress(privateKey: privateKey.toHexString())
.publicKeyBytes
else {
throw WalletError.failedToGetPublicKey
}
return try Account.getCompressedPublicKeyFrom(uncompressedPublicKey: uncompressedPublicKey)
}

public static func getCompressedPublicKeyFrom(uncompressedPublicKey: Data) throws -> Data {
// Ensure the input key is exactly 64 bytes
guard uncompressedPublicKey.count == 64 else {
throw AccountError.invalidPrivateKey
}

// Extract x and y coordinates
let x = uncompressedPublicKey.prefix(32) // First 32 bytes are x
let y = uncompressedPublicKey.suffix(32) // Last 32 bytes are y

// Convert y to an integer to determine parity
let yInt = BigInt(y.toHex(), radix: 16)!
let isEven = yInt % 2 == 0

// Determine the prefix based on the parity of y
let prefix: UInt8 = isEven ? 0x02 : 0x03

// Construct the compressed key: prefix + x
var compressedKey = Data([prefix]) // Start with the prefix
compressedKey.append(x) // Append the x-coordinate

return compressedKey
}

public static func privateKeyObjectFromKey(_ key: Data) throws -> secp256k1.Signing.PrivateKey {
return try secp256k1.Signing.PrivateKey(
dataRepresentation: key, format: .uncompressed)
}
}
3 changes: 2 additions & 1 deletion Sources/XyoClient/Address/AccountInstance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ public protocol AccountInstance: PrivateKeyInstance {
var previousHash: Hash? { get }
var privateKey: Data? { get }
var publicKey: Data? { get }

var publicKeyUncompressed: Data? { get }

}
1 change: 0 additions & 1 deletion Sources/XyoClient/Address/XyoAddress.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,3 @@ public class XyoAddress {
}
}
}

11 changes: 8 additions & 3 deletions Sources/XyoClient/Api/Archivist/ArchivistApiClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ public class XyoArchivistApiClient {
ProcessInfo.processInfo.environment["XYO_API_MODULE"] ?? "Archivist"

private static let ArchivistInsertQuerySchema = "network.xyo.query.archivist.insert"
private static let ArchivistInsertQuery: EncodablePayloadInstance = EncodablePayloadInstance(ArchivistInsertQuerySchema)
private static let ArchivistInsertQuery: EncodablePayloadInstance = EncodablePayloadInstance(
ArchivistInsertQuerySchema)

let config: XyoArchivistApiConfig
let queryAccount: AccountInstance
Expand Down Expand Up @@ -66,7 +67,9 @@ public class XyoArchivistApiClient {
)

// Check if the response data matches the expected result
if decodedResponse.data?.bw.typedPayload.payload_hashes.count == payloads.count {
if decodedResponse.data?.bw.typedPayload.payload_hashes.count
== payloads.count
{
// Return the payloads array in case of success
completion(payloads, nil)
} else {
Expand All @@ -91,7 +94,9 @@ public class XyoArchivistApiClient {
}

@available(iOS 15, *)
public func insert(payloads: [EncodablePayloadInstance]) async throws -> [EncodablePayloadInstance] {
public func insert(payloads: [EncodablePayloadInstance]) async throws
-> [EncodablePayloadInstance]
{
// Build QueryBoundWitness
let (bw, signed) = try BoundWitnessBuilder()
.payloads(payloads)
Expand Down
11 changes: 7 additions & 4 deletions Sources/XyoClient/BoundWitness/BoundWitness.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ public protocol EncodableBoundWitness: EncodablePayload, BoundWitnessFields, Enc

public protocol BoundWitness: EncodableBoundWitness, EncodablePayload, Payload, Codable {}

public class BoundWitnessInstance: PayloadInstance
{
public class BoundWitnessInstance: PayloadInstance {
public var signatures: [String]? = nil

public var addresses: [String] = []
Expand Down Expand Up @@ -62,6 +61,10 @@ public class BoundWitnessInstance: PayloadInstance
}
}

public typealias EncodableBoundWitnessWithMeta = EncodableWithCustomMetaInstance<BoundWitnessInstance, BoundWitnessMeta>
public typealias EncodableBoundWitnessWithMeta = EncodableWithCustomMetaInstance<
BoundWitnessInstance, BoundWitnessMeta
>

public typealias BoundWitnessWithMeta = WithCustomMetaInstance<BoundWitnessInstance, BoundWitnessMeta>
public typealias BoundWitnessWithMeta = WithCustomMetaInstance<
BoundWitnessInstance, BoundWitnessMeta
>
8 changes: 5 additions & 3 deletions Sources/XyoClient/BoundWitness/BoundWitnessBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ public class BoundWitnessBuilder {
return self
}

public func payload<T: EncodablePayloadInstance>(_ schema: String, _ payload: T) throws -> BoundWitnessBuilder {
public func payload<T: EncodablePayloadInstance>(_ schema: String, _ payload: T) throws
-> BoundWitnessBuilder
{
_payloads.append(payload)
_payload_hashes.append(try PayloadBuilder.dataHash(from: payload))
_payload_schemas.append(schema)
Expand Down Expand Up @@ -60,14 +62,14 @@ public class BoundWitnessBuilder {
public func build() throws -> (EncodableBoundWitnessWithMeta, [EncodablePayloadInstance]) {
let bw = BoundWitnessInstance()
bw.addresses = _accounts.map { account in account.address!.toHex() }
bw.previous_hashes = _previous_hashes.map { hash in hash?.toHex()}
bw.previous_hashes = _previous_hashes.map { hash in hash?.toHex() }
bw.payload_hashes = _payload_hashes.map { hash in hash.toHex() }
bw.payload_schemas = _payload_schemas
if _query != nil {
bw.query = _query?.toHex()
}
let dataHash = try PayloadBuilder.dataHash(from: bw)
let signatures = try self.sign(hash: dataHash).map {signature in signature.toHex()}
let signatures = try self.sign(hash: dataHash).map { signature in signature.toHex() }
let meta = BoundWitnessMeta(signatures)
let bwWithMeta = EncodableWithCustomMetaInstance(from: bw, meta: meta)
return (bwWithMeta, _payloads)
Expand Down
6 changes: 3 additions & 3 deletions Sources/XyoClient/BoundWitness/Meta/BoundWitnessMeta.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ public protocol BoundWitnessMetaProtocol: Decodable {
public class BoundWitnessMeta: BoundWitnessMetaProtocol, Decodable, Encodable {
public var client: String?
public var signatures: [String]?

enum CodingKeys: String, CodingKey {
case client
case signatures
}

public init(_ signatures: [String] = []) {
self.client = "ios"
self.signatures = signatures
}

public required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
client = try values.decode(String.self, forKey: .client)
Expand Down
5 changes: 4 additions & 1 deletion Sources/XyoClient/Module/ModuleQueryResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ public class ModuleQueryResult: Codable {
public let bw: EncodableBoundWitnessWithMeta
public let payloads: [EncodablePayloadInstance]
public let errors: [EncodablePayloadInstance]
init(bw: EncodableBoundWitnessWithMeta, payloads: [EncodablePayloadInstance] = [], errors: [EncodablePayloadInstance] = []) {
init(
bw: EncodableBoundWitnessWithMeta, payloads: [EncodablePayloadInstance] = [],
errors: [EncodablePayloadInstance] = []
) {
self.bw = bw
self.payloads = payloads
self.errors = errors
Expand Down
27 changes: 27 additions & 0 deletions Sources/XyoClient/Payload/Core/Id.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
open class IdPayload: EncodablePayloadInstance {

public static let schema: String = "network.xyo.id"

var salt: String

public override init(_ salt: String) {
self.salt = salt
super.init(IdPayload.schema)
}

public convenience init(_ salt: UInt) {
let s = "\(salt)"
self.init(s)
}

enum CodingKeys: String, CodingKey {
case salt
case schema
}

override open func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.schema, forKey: .schema)
try container.encode(self.salt, forKey: .salt)
}
}
Loading

0 comments on commit 5fe807e

Please sign in to comment.