Skip to content

Commit

Permalink
Add ability to import owned tokens to resolution-swift library (#44)
Browse files Browse the repository at this point in the history
* tokensOwnedBy
  • Loading branch information
JohnnyJumper authored Jun 23, 2021
1 parent a1cbfea commit 9ca3808
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
BlueprintIdentifier = "3EE4DA0224E367720097540B"
BuildableName = "Resolution.framework"
BlueprintName = "UnstoppableDomainsResolution"
ReferencedContainer = "container:Resolution.xcodeproj">
ReferencedContainer = "container:resolution.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
Expand All @@ -35,7 +35,7 @@
BlueprintIdentifier = "3EE4DA0B24E367720097540B"
BuildableName = "ResolutionTests.xctest"
BlueprintName = "ResolutionTests"
ReferencedContainer = "container:Resolution.xcodeproj">
ReferencedContainer = "container:resolution.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
Expand Down Expand Up @@ -63,7 +63,7 @@
BlueprintIdentifier = "3EE4DA0224E367720097540B"
BuildableName = "Resolution.framework"
BlueprintName = "UnstoppableDomainsResolution"
ReferencedContainer = "container:Resolution.xcodeproj">
ReferencedContainer = "container:resolution.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
Expand Down
43 changes: 43 additions & 0 deletions Sources/UnstoppableDomainsResolution/ABI/JSON_RPC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,23 @@ public struct JsonRpcPayload: Codable {
ParamElement.string("latest")
])
}

init (params: ParamLogClass) {
self.init(
jsonrpc: "2.0",
id: "1.0",
method: "eth_getLogs",
params: [
ParamElement.paramLogClass(params)
]
)
}
}

public enum ParamElement: Codable {
case paramClass(ParamClass)
case paramLogClass(ParamLogClass)
case paramLogResponse(JsonRpcLogResponse)
case string(String)
case array([ParamElement])
case dictionary([String: ParamElement])
Expand All @@ -53,6 +66,14 @@ public enum ParamElement: Codable {
self = .paramClass(elem)
return
}
if let elem = try? container.decode(ParamLogClass.self) {
self = .paramLogClass(elem)
return
}
if let elem = try? container.decode(JsonRpcLogResponse.self) {
self = .paramLogResponse(elem)
return
}
if let elem = try? container.decode(Array<ParamElement>.self) {
self = .array(elem)
return
Expand All @@ -74,6 +95,10 @@ public enum ParamElement: Codable {
switch self {
case .paramClass(let elem):
try container.encode(elem)
case .paramLogClass(let elem):
try container.encode(elem)
case .paramLogResponse(let elem):
try container.encode(elem)
case .string(let elem):
try container.encode(elem)
case .array(let array):
Expand All @@ -89,6 +114,24 @@ public struct ParamClass: Codable {
let to: String
}

public struct ParamLogClass: Codable {
let fromBlock: String
let address: String
let topics: [String?]
}

public struct JsonRpcLogResponse: Codable {
let address: String
let blockHash: String
let blockNumber: String
let data: String
let logIndex: String
let removed: Bool
let topics: [String]
let transactionHash: String
let transactionIndex: String
}

public struct JsonRpcResponse: Decodable {
let jsonrpc: String
let id: String
Expand Down
46 changes: 37 additions & 9 deletions Sources/UnstoppableDomainsResolution/Contract.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ internal class Contract {
func callMethod(methodName: String, args: [Any]) throws -> Any {
let encodedData = try self.coder.encode(method: methodName, args: args)
let body = JsonRpcPayload(id: "1", data: encodedData, to: address)
let response = try postRequest(body)!
let response = try postRequestForString(body)!
return try self.coder.decode(response, from: methodName)
}

Expand All @@ -58,9 +58,42 @@ internal class Contract {
}
return IdentifiableResult<Any?>(id: responseElement.id, result: res)
}
}
}

func callLogs(fromBlock: String, signatureHash: String, for userAddress: String, isTransfer: Bool ) throws -> [JsonRpcLogResponse] {
let topics = isTransfer ? [signatureHash, nil, userAddress ] : [signatureHash, userAddress]
let params = ParamLogClass(fromBlock: fromBlock, address: self.address, topics: topics)
let body = JsonRpcPayload(params: params)

private func postRequest(_ body: JsonRpcPayload) throws -> String? {
return try postRequestForLogArray(body) ?? []
}

private func postRequestForLogArray(_ body: JsonRpcPayload) throws -> [JsonRpcLogResponse]? {
let postResponse = try postRequest(body)
if case .array(let arrayOfElements) = postResponse {
return arrayOfElements.compactMap {
switch $0 {
case .paramLogResponse(let val):
return val
case _:
return nil
}
}
}
return []
}

private func postRequestForString(_ body: JsonRpcPayload) throws -> String? {
let postResponse = try postRequest(body)
switch postResponse {
case .string(let result):
return result
default:
return nil
}
}

private func postRequest(_ body: JsonRpcPayload) throws -> ParamElement? {
let postRequest = APIRequest(providerUrl, networking: networking)
var resp: JsonRpcResponseArray?
var err: Error?
Expand All @@ -78,12 +111,7 @@ internal class Contract {
guard err == nil else {
throw err!
}
switch resp?[0].result {
case .string(let result):
return result
default:
return nil
}
return resp?[0].result
}

private func postBatchRequest(_ bodyArray: [JsonRpcPayload]) throws -> [IdentifiableResult<String>?] {
Expand Down
8 changes: 8 additions & 0 deletions Sources/UnstoppableDomainsResolution/ContractZNS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ internal class ContractZNS {
dict[key] = self.map(array: array)
case .dictionary(let dictionary):
dict[key] = self.reduce(dict: dictionary)
case .paramLogClass(let elem):
dict[key] = elem
case .paramLogResponse(let elem):
dict[key] = elem
}
}
}
Expand All @@ -87,6 +91,10 @@ internal class ContractZNS {
switch value {
case .paramClass(let elem):
return elem
case .paramLogClass(let elem):
return elem
case .paramLogResponse(let elem):
return elem
case .string(let elem):
return elem
case .array(let array):
Expand Down
82 changes: 82 additions & 0 deletions Sources/UnstoppableDomainsResolution/NamingServices/CNS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ internal class CNS: CommonNamingService, NamingService {

static let specificDomain = ".crypto"
static let name = "CNS"
static let TransferEventSignature = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
static let NewURIEventSignature = "0xc5beef08f693b11c316c0c8394a377a0033c9cf701b8cd8afd79cecef60c3952"

static let getDataForManyMethodName = "getDataForMany"

Expand Down Expand Up @@ -91,6 +93,44 @@ internal class CNS: CommonNamingService, NamingService {
}
}

func tokensOwnedBy(address: String) throws -> [String] {
let registryContract = try self.buildContract(address: self.contracts.registryAddress, type: .registry)
let origin = self.getOriginBlockFrom(network: self.network)

let transferLogs = try registryContract.callLogs(
fromBlock: origin,
signatureHash: Self.TransferEventSignature,
for: address.normalized32,
isTransfer: true
).compactMap {
$0.topics[3]
}

let domainsData = try transferLogs.compactMap {
try registryContract.callLogs(
fromBlock: origin,
signatureHash: Self.NewURIEventSignature,
for: $0.normalized32,
isTransfer: false
)[0].data
}

let possibleDomains = Array(Set(
domainsData.compactMap {
ABIDecoder.decodeSingleType(type: .string, data: Data(hex: $0)).value as? String
}
)
)

let owners = try batchOwners(domains: possibleDomains)
var domains: [String] = []

for (ind, addr) in owners.enumerated() where addr == address {
domains.append(possibleDomains[ind])
}
return domains
}

func resolver(domain: String) throws -> String {
let tokenId = super.namehash(domain: domain)
return try self.resolver(tokenId: tokenId)
Expand Down Expand Up @@ -192,6 +232,17 @@ internal class CNS: CommonNamingService, NamingService {
return nil
}

private func getOriginBlockFrom(network: String) -> String {
switch network {
case "mainnet":
return "0x8A958B"
case "rinkeby":
return "0x7232BC"
default:
return "earliest"
}
}

private func getOwnerResolverRecord(tokenId: String, key: String) throws -> OwnerResolverRecord {
let res = try self.getDataForMany(keys: [key], for: [tokenId])
if let dict = res as? [String: Any] {
Expand Down Expand Up @@ -222,3 +273,34 @@ internal class CNS: CommonNamingService, NamingService {
throw ResolutionError.proxyReaderNonInitialized
}
}

fileprivate extension String {
var normalized32: String {
let droppedHexPrefix = self.hasPrefix("0x") ? String(self.dropFirst("0x".count)) : self
let cleanAddress = droppedHexPrefix.lowercased()
if cleanAddress.count < 64 {
let zeroCharacter: Character = "0"
let arr = Array(repeating: zeroCharacter, count: 64 - cleanAddress.count)
let zeros = String(arr)

return "0x" + zeros + cleanAddress
}
return "0x" + cleanAddress
}
}

fileprivate extension Data {
init?(hex: String) {
guard hex.count.isMultiple(of: 2) else {
return nil
}

let chars = hex.map { $0 }
let bytes = stride(from: 0, to: chars.count, by: 2)
.map { String(chars[$0]) + String(chars[$0 + 1]) }
.compactMap { UInt8($0, radix: 16) }

guard hex.count / bytes.count == 2 else { return nil }
self.init(bytes)
}
}
4 changes: 4 additions & 0 deletions Sources/UnstoppableDomainsResolution/NamingServices/ZNS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ internal class ZNS: CommonNamingService, NamingService {
throw ResolutionError.methodNotSupported
}

func tokensOwnedBy(address: String) throws -> [String] {
throw ResolutionError.methodNotSupported
}

func addr(domain: String, ticker: String) throws -> String {
let key = "crypto.\(ticker.uppercased()).address"
let result = try record(domain: domain, key: key)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ protocol NamingService {
func isSupported(domain: String) -> Bool

func owner(domain: String) throws -> String
func tokensOwnedBy(address: String) throws -> [String]
func addr(domain: String, ticker: String) throws -> String
func resolver(domain: String) throws -> String

Expand Down
18 changes: 18 additions & 0 deletions Sources/UnstoppableDomainsResolution/Resolution.swift
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,24 @@ public class Resolution {
}
}

/// Returns domains owned by `address`
/// - Parameter address: - address of a user you want to get domains of
/// - Parameter service: - name of the service you want to check agains CNS or ZNS
/// - Parameter completion: - A callback taht resolves `Result` with an `array of domain names` or `Error`
public func tokensOwnedBy(address: String, service: String, completion: @escaping StringsArrayResultConsumer ) {
DispatchQueue.global(qos: .utility).async { [weak self] in
do {
guard let service = self?.services.first(where: { $0.name == service }) else {
throw ResolutionError.unsupportedServiceName
}
let result = try service.tokensOwnedBy(address: address)
completion(.success(result))
} catch {
self?.catchError(error, completion: completion)
}
}
}

/// Resolves a resolver address of a `domain`
/// - Parameter domain: - domain name to be resolved
/// - Parameter completion: A callback that resolves `Result` with a `resolver address` for a specific domain or `Error`
Expand Down
Loading

0 comments on commit 9ca3808

Please sign in to comment.