Elegant Web3js functionality in Swift. Native ABI parsing and smart contract interactions.
Full Changelog: https://github.com/skywinder/web3swift/compare/3.0.0...3.0.1
This release changes whole way of iterating with Etherum chain in a terms of creating, preparing, sending transaction and receiving receipt on it. Now it’s:
CodableTransaction
to handle[Read|WriteOperation]
.Any
.Previously there were four (!) types that is being used to prepare and send transaction:
EthereumTransaction
TransactionParameters
EthereumParameters
TransactionOptions
Now their all gone and there’s only one to have: CodableTransaction
, this one covers all previous usecases, because it’s actually just all in one merged yet.
Unfortunately it’s not as clean as i wish it would be, but despite hurting my sense of perfection it’s much, much more better way to work with that it was before.
So now to create transaction that you would send to a node further you sould just do the follow:
let transaction: CodableTransaction = .emptyTransaction
transaction.from = from ?? transaction.sender // `sender` one is if you have private key of your wallet address, so public key e.g. your wallet address could be infereted
transaction.value = value
transaction.gasLimitPolicy = .manual(78423)
transaction.gasPricePolicy = .manual(20000000000)
Web3.eth.send(transaction)
CodableTransaction
overviewSo let’s take a look at CodableTransaction
most important properties closer:
{ get set }
:
from: EthereumAddress?
— from property to fill it where it needs to.to: EthereumAddress
— adressee of a transaction, the only required property on membervise initnalizaion.type: TransactionType
— Type of transaction in Ethereum mainnet mean, 0 — legacy, 1 — post EIP-2930, 2 — post EIP-1559.chainID: BigUInt?
— The chainId that transaction is targeted for.value: BigUInt
— The native value of a transaction.data: Data
— data property of a trancaction, most used whilist contract iterating.callOnBlock: BlockNumber?
— Block number to call a given transaction on.accessList: [AccessListEntry]?
— access list for contract execution (EIP-2930 and EIP-1559 only).{ get set }
(could be changed in release):
nonce: BigUInt { get }
noncePolicy: NoncePolicy { set }
gasLimit: BigUInt { get }
gasLimitPolicy: GasLimitPolicy { set }
gasPrice: BigUInt? { get }
gasPricePolicy: GasPricePolicy { set }
maxFeePerGas: BigUInt? { get }
maxFeePerGasPolicy: FeePerGasPolicy { set }
maxPriorityFeePerGas: BigUInt? { get }
maxPriorityFeePerGasPolicy: FeePerGasPolicy { set }
{ get }
:
meta: EthereumMetadata?
— this one provides some information about already executed transaction, such is blockHash
, transactionIndex
, gasprice
and some related.sender: EthereumAddress?
— the address of the sender of the transaction recovered from the signature.hash: Data?
— A transaction hash.v: BigUInt
— signature v component.r: BigUInt
— signature r component.s: BigUInt
— signature s component.Second big chunk of improvement has happened with [Read|Write]Transaction
types. So this is actually the general type to iterate with contracts. While doing so you’ve constantly creating and passing this and there this struct, which is encapsulate in itself all contract related data (contract
object, method
that you willing to call, transaction
to be sent to). So this those structs are now renamed into [Read|Write]Operation
respectively, so as their methods. Dispite that all pipeline is kept the same, so to make some read contract call you have to do follow:
let contract = Web3.contract(Web3.Utils.erc20ABI, at: receipt.contractAddress!)!
let readOp = contract.createReadOperation("name")!
readOp.transaction.from = EthereumAddress("0xe22b8979739D724343bd002F9f432F5990879901")
let response = try await readTX.callContractMethod()
and for write call follow:
let abiString = "[]" // some ABI string
let bytecode = Data.fromHex("") // some ABI bite sequence
let contract = Web3.contract(abiString, at: nil, abiVersion: 2)!
let parameters = [...] as [AnyObject]
let deployOp = contract.prepareDeploy(bytecode: bytecode, constructor: contract.contract.constructor, parameters: parameters)!
deployOp.transaction.from = "" // your address
deployOp.transaction.gasLimitPolicy = .manual(3000000)
let result = try await deployTx.writeToChain(password: "web3swift")
You haven’t look under the hood before, but i did, and it was scary one. It suddenly appears that resolving and managing nothing, but five objects to send something peace of bite a chain is not as easy as it might be thoughts, so there was a mess in both method intents and their implementation.
Now most of it’s gone, and the rule number one: You should not merge any transaction by yourself.
CodableTransaction
property in it.CodableTransaction
property in it too.Both these properties named transaction
and one assigns another, when Contract
creates appropriate Operation
to be sent.
So thil all leads to rule number two: You should work to any you would like, but only one transaction object. Please pay attention to it, it’s still possible to passing your transaction all around your code while filling it in, but it’s strongly not recomendet. The better way is to choose one place where CodableTransaction
instance would be filled and implement utility methods that filled it in with appropriate data, like BigUInt
for gasWhatever
and BlockNumber
for callOnBlock
properties.
There’s totally new network layer API. It based on generic implementation and on some cascade of protocols. Let’s take a closer look in it.
Using new API is as easy as write three line of a code:
func feeHistory(blockCount: UInt, block: BlockNumber, percentiles:[Double]) async throws -> Web3.Oracle.FeeHistory {
let requestCall: APIRequest = .feeHistory(blockCount, block, percentiles)
let response: APIResponse<Web3.Oracle.FeeHistory> = try await APIRequest.sendRequest(with: Web3.provider, for: requestCall) /// explicitly declaring `Result` type is **required**.
return response.result
}
Result
type and making a network request.And that’s it, you’re done here.
Thanks to @mliot`s work it’s under the hood transaction signing/decoding with respect to its type.
So actually you don’t need to feel data
property by yourself in most of cases, because it happening itself and it happening on WriteOperation.writeToChain
and ReadOperation.callContractMethod
e.g. on the very end of the whole pipeline[^1].
So let’s recap new available API pipelines.
let transaction: CodableTransaction = .emptyTransaction
transaction.from = from ?? transaction.sender // `sender` one is if you have private key of your wallet address, so public key e.g. your wallet address could be infereted
transaction.value = value
transaction.gasLimitPolicy = .manual(78423)
transaction.gasPricePolicy = .manual(20000000000)
Web3.eth.send(transaction)
let contract = Web3.contract(Web3.Utils.erc20ABI, at: receipt.contractAddress!)!
let readOp = contract.createReadOperation("name")!
readOp.transaction.from = EthereumAddress("0xe22b8979739D724343bd002F9f432F5990879901")
let response = try await readTX.callContractMethod()
let abiString = "[]" // some ABI string
let bytecode = Data.fromHex("") // some ABI bite sequence
let contract = Web3.contract(abiString, at: nil, abiVersion: 2)!
let parameters = [...] as [AnyObject]
let deployOp = contract.prepareDeploy(bytecode: bytecode, constructor: contract.contract.constructor, parameters: parameters)!
deployOp.transaction.from = "" // your address
deployOp.transaction.gasLimitPolicy = .manual(3000000)
let result = try await deployTx.writeToChain(password: "web3swift")
func feeHistory(blockCount: UInt, block: BlockNumber, percentiles:[Double]) async throws -> Web3.Oracle.FeeHistory {
let requestCall: APIRequest = .feeHistory(blockCount, block, percentiles)
let response: APIResponse<Web3.Oracle.FeeHistory> = try await APIRequest.sendRequest(with: Web3.provider, for: requestCall) /// explicitly declaring `Result` type is **required**.
return response.result
}
[^1]: This is actually not a whole true, because in that stage it’s just all gas related properties gets resolved.
This PR changes whole way of creating, preparing, sending transaction and receiving receipt on it. Now it’s:
CodableTransaction
to handle[Read|WriteOperation]
.Previously there were four (!) types that is being used to prepare and send transaction:
EthereumTransaction
TransactionParameters
EthereumParameters
TransactionOptions
Now their all gone and there’s only one to have: CodableTransaction
, this one covers all previous usecases, because it’s actually just all in one merged yet.
Unfortunately it’s not as clean as i wish it would be, but despite hurting my sense of perfection it’s much, much more better way to work with that it was before.
So now to create transaction that you would send to a node further you sould just do the follow:
let transaction: CodableTransaction = .emptyTransaction
transaction.from = from ?? transaction.sender // `sender` one is if you have private key of your wallet address, so public key e.g. your wallet address could be infereted
transaction.value = value
transaction.gasLimitPolicy = .manual(78423)
transaction.gasPricePolicy = .manual(20000000000)
web3.eth.send(transaction)
CodableTransaction
overviewSo let’s take a look at CodableTransaction
most important properties closer:
{ get set }
:
from: EthereumAddress?
— from property to fill it where it needs to.to: EthereumAddress
— adressee of a transaction, the only required property on membervise initnalizaion.type: TransactionType
— Type of transaction in Ethereum mainnet mean, 0 — legacy, 1 — post EIP-2930, 2 — post EIP-1559.chainID: BigUInt?
— The chainId that transaction is targeted for.value: BigUInt
— The native value of a transaction.data: Data
— data property of a trancaction, most used whilist contract iterating.callOnBlock: BlockNumber?
— Block number to call a given transaction on.accessList: [AccessListEntry]?
— access list for contract execution (EIP-2930 and EIP-1559 only).{ get set }
(could be changed in release):
nonce: BigUInt { get }
noncePolicy: NoncePolicy { set }
gasLimit: BigUInt { get }
gasLimitPolicy: GasLimitPolicy { set }
gasPrice: BigUInt? { get }
gasPricePolicy: GasPricePolicy { set }
maxFeePerGas: BigUInt? { get }
maxFeePerGasPolicy: FeePerGasPolicy { set }
maxPriorityFeePerGas: BigUInt? { get }
maxPriorityFeePerGasPolicy: FeePerGasPolicy { set }
{ get }
:
meta: EthereumMetadata?
— this one provides some information about already executed transaction, such is blockHash
, transactionIndex
, gasprice
and some related.sender: EthereumAddress?
— the address of the sender of the transaction recovered from the signature.hash: Data?
— A transaction hash.v: BigUInt
— signature v component.r: BigUInt
— signature r component.s: BigUInt
— signature s component.Second big chunk of improvement has happened with [Read|Write]Transaction
types. So this is actually the general type to iterate with contracts. While doing so you’ve constantly creating and passing this and there this struct, which is encapsulate in itself all contract related data (contract
object, method
that you willing to call, transaction
to be sent to). So this those structs are now renamed into [Read|Write]Operation
respectively, so as their methods. Dispite that all pipeline is kept the same, so to make some read contract call you have to do follow:
let contract = web3.contract(Web3.Utils.erc20ABI, at: receipt.contractAddress!)!
let readOp = contract.createReadOperation("name")!
readOp.transaction.from = EthereumAddress("0xe22b8979739D724343bd002F9f432F5990879901")
let response = try await readTX.callContractMethod()
and for write call follow:
let abiString = "[]" // some ABI string
let bytecode = Data.fromHex("") // some ABI bite sequence
let contract = web3.contract(abiString, at: nil, abiVersion: 2)!
let parameters = [...] as [AnyObject]
let deployOp = contract.prepareDeploy(bytecode: bytecode, constructor: contract.contract.constructor, parameters: parameters)!
deployOp.transaction.from = "" // your address
deployOp.transaction.gasLimitPolicy = .manual(3000000)
let result = try await deployTx.writeToChain(password: "web3swift")
You haven’t look under the hood before, but i did, and it was scary one. It suddenly appears that resolving and managing nothing, but five objects to send something peace of bite a chain is not as easy as it might be thoughts, so there was a mess in both method intents and their implementation.
Now most of it’s gone, and the rule number one: You should not merge any transaction by yourself.
CodableTransaction
property in it.CodableTransaction
property in it too.Both these properties named transaction
and one assigns another, when Contract
creates appropriate Operation
to be sent.
So thil all leads to rule number two: You should work to any you would like but only one transaction object. Please pay attention to it, it’s still possible to passing your transaction all around your code while filling it in, but it’s strongly not recomendet. The better way is to choose one place where CodableTransaction
instance would be filled and implement utility methods that filled it in with appropriate data, like BigUInt
for gasWhatever
and BlockNumber
for callOnBlock
properties.
Thanks to @mliot`s work it’s under the hood transaction signing/decoding with respect to its type.
So actually you don’t need to feel data
property by yourself in most of cases, because it happening itself and it happening on WriteOperation.writeToChain
and ReadOperation.callContractMethod
e.g. on the very end of the whole pipeline[^1].
There’s a something left to do, to make all it production ready, but it wouldn’t be any revolutionary in terms of API design either main pipelines stages. So let’s recap new available API pipelines.
let transaction: CodableTransaction = .emptyTransaction
transaction.from = from ?? transaction.sender // `sender` one is if you have private key of your wallet address, so public key e.g. your wallet address could be infereted
transaction.value = value
transaction.gasLimitPolicy = .manual(78423)
transaction.gasPricePolicy = .manual(20000000000)
web3.eth.send(transaction)
let contract = web3.contract(Web3.Utils.erc20ABI, at: receipt.contractAddress!)!
let readOp = contract.createReadOperation("name")!
readOp.transaction.from = EthereumAddress("0xe22b8979739D724343bd002F9f432F5990879901")
let response = try await readTX.callContractMethod()
let abiString = "[]" // some ABI string
let bytecode = Data.fromHex("") // some ABI bite sequence
let contract = web3.contract(abiString, at: nil, abiVersion: 2)!
let parameters = [...] as [AnyObject]
let deployOp = contract.prepareDeploy(bytecode: bytecode, constructor: contract.contract.constructor, parameters: parameters)!
deployOp.transaction.from = "" // your address
deployOp.transaction.gasLimitPolicy = .manual(3000000)
let result = try await deployTx.writeToChain(password: "web3swift")
[^1]: This is actually not whole true, because in that stage it’s just all gas related properties gets resolved.
Full Changelog: https://github.com/skywinder/web3swift/compare/3.0.0-RC4...3.0.0-RC5
Full Changelog: https://github.com/skywinder/web3swift/compare/3.0.0-RC3...3.0.0-RC4
Now there's separate module Core
which implements network methods released in previous RC and base structures.
Also some refactoring and code cleaning have been done.
Also there were applied PR review fixups for previous release (thanks to @JeneaVranceanu)
Full Changelog: https://github.com/skywinder/web3swift/compare/3.0.0-RC2...3.0.0-RC3
Full Changelog: https://github.com/skywinder/web3swift/compare/2.6.5...2.6.6
Using new API is as easy as write three line of a code:
func feeHistory(blockCount: UInt, block: BlockNumber, percentiles:[Double]) async throws -> Web3.Oracle.FeeHistory {
let requestCall: APIRequest = .feeHistory(blockCount, block, percentiles)
let response: APIResponse<Web3.Oracle.FeeHistory> = try await APIRequest.sendRequest(with: web3.provider, for: requestCall) /// explicitly declaring `Result` type is **required**.
return response.result
}
Result
type and making a network request.And that’s it, you’re done here.
There’s follow types have been implemented
public enum APIRequest
This is the main type of whole network layer API. It responsible to both compose API request to a node and to send it to a node with a given provider (which stores Node URL and session), as cases it have all available JSON RPC requests and most of them have associated values to pass request parameters there.
Additionally it have follow computed properties:
public responseType: APIResultType.Type
- this variable returns appropriate Result
generic parameter type for each API call. Which can be split generally in two parts:
Int
, BigInt
) which could not be extended on client side.Block
) which could be extended on client side. That said that user able to add additional Result
type on their side if it’s not literal (e.g. if it’s a struct
or class
).method: REST
- this internal variable returns REST method for each API call. Currently its returning only POST
one.parameters: [RequestParameter]
- this internal variable is purposed to return parameters of request as an heterogeneous Array which is Node expected in most cases.encodedBody: Data
- this internal variable returns encoded data of RequestBody
type, which is required to compose correct request to a Node.call: String
- this internal variable returns method call string, which is one of property of RequestBody
type.There’s two methods are provided for API calls.
public static func sendRequest<Result>(with provider: Web3Provider, for call: APIRequest) async throws -> APIResponse<Result>
- this method is the main one. It composes and sends request to a Node. This method could be called only with explicitly return type declaration like let response: APIResponse<BigUInt> = try await APIRequest.sendRequest(with: self.provider, for: .gasPrice)
, where response
is the whole APIResponse struct with a service properties and the response.result
is a point of interests in example above — gas price.static func setupRequest(for call: APIRequest, with provider: Web3Provider) -> URLRequest
- this internal method is just composing network request from all properties of related APIRequest
case.public struct APIResponse<Result>: Decodable where Result: APIResultType
This generic struct is any Ethereum node response container, where all stored properties are utility fields and one generic result: Result
is the property that stores strictly typed result of any given request.
To make things work there’s are some protocols be presented which both adds restriction to types that could be passed within new API and add some common methods to Literal types to be able initialize it from a hex string.
APIResultType
This protocol responsible for any nonliteral type that could be stored within APIResponse<Result>
generic struct. This protocol have no requirements except it conforms Decodable
protocol. So any decodable type could be extended to conforms it.
LiteralInitiableFromString
This protocol responsible for any literal types that could be stored within APIResponse<Result>
. This protocol conforms APIResultType
and it adds some requirements to it, like initializer from hex string. Despite that a given type could be extended to implement such initializer ==this should be done on a user side== because to make it work it requires some work within sendRequest
method to be done.
IntegerInitableWithRadix
This protocol is just utility one, which declares some convenient initializer which have both Int
and BigInt
types, but don’t have any common protocol which declares such requirement.
struct RequestBody: Encodable
— just a request body that passes into request body.public enum REST: String
— enum of REST methods. Only POST
and GET
presented yet.enum RequestParameter
— this enum is a request composing helper. It make happened to encode request attribute as heterogeneous array.protocol APIRequestParameterType: Encodable
— this type is part of the RequestParameter
enum mechanism which purpose is to restrict types that can be passed as associated types within RequestParameter
cases.protocol APIRequestParameterElementType: Encodable
— this type purpose is the same as APIRequestParameterType
one except this one is made for Element
s of an Array
s when the latter is an associated type of a given RequestParameter
case.Full Changelog: https://github.com/skywinder/web3swift/compare/3.0.0-RC1...3.0.0-RC2
from
in transactionOptions
by @mloit in https://github.com/skywinder/web3swift/pull/570
Full Changelog: https://github.com/skywinder/web3swift/compare/2.6.4...2.6.5
NOTE: This is the first 3.0.0 release, e.g. it's alpha. We're not guarantee any reliability neither API stability in further versions.
Promise
concurrency types from the library and replace them to modern async/await
concurrency mechanism.async/await
).Carthage
dependency manager, on 3.0.0-RC2
it'll be dropped due to its low prevalence.unstable
branch by @mloit in https://github.com/skywinder/web3swift/pull/559
Full Changelog: https://github.com/skywinder/web3swift/compare/2.6.4...3.0.0-RC1
fromJSON()
functions by @mloit in https://github.com/skywinder/web3swift/pull/558
Full Changelog: https://github.com/skywinder/web3swift/compare/2.6.3...2.6.4