Skip to content

Instantly share code, notes, and snippets.

@learner-long-life
Created August 7, 2017 06:35
Show Gist options
  • Select an option

  • Save learner-long-life/fbbff456e54fa2a51214c5075daa0626 to your computer and use it in GitHub Desktop.

Select an option

Save learner-long-life/fbbff456e54fa2a51214c5075daa0626 to your computer and use it in GitHub Desktop.
How to Upload and Initialize an Ethereum Crowdfund

How to Upload and Initialize an Ethereum Crowdfund

This writeup will tell you how to deploy (upload) a crowdfunding contract, first on the Rinkeby testnet, and then when you have confidence that it will behave correctly, on the Etherem mainnet.

We've assumed that you've gone through a previous walkthrough, where you're running a Rinkeby node locally and have 3 ETH to play with.

Preparation

In one terminal, first clone the Democracy sources, which include a simple crowdfunding contract called ZcashEscrow.

git clone https://github.com/invisible-college/democracy.git

In another terminal, attach to your geth node:

geth attach

Uploading

Unlock your account with enough ETH in it to upload the contract (1 ETH should be plenty). Copy and paste the below, long command into the geth console:

var browser_leanfund_sol_leanfundContract = web3.eth.contract([{"constant":false,"inputs":[],"name":"safeKill","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"payoutsETH","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"beneficiary","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"contributionsETH","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"version","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"payoutETH","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"fundingGoal","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"amountRaised","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"deadlineBlockNumber","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"creationTime","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"fee","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"getContribution","outputs":[{"name":"retVal","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"feeWithdrawn","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"open","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"safeWithdrawal","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_fundingGoalInWei","type":"uint256"},{"name":"_beneficiary","type":"address"},{"name":"_deadlineBlockNumber","type":"uint256"}],"name":"initialize","outputs":[],"payable":false,"type":"function"},{"inputs":[],"payable":false,"type":"constructor"},{"payable":true,"type":"fallback"}]);
var browser_leanfund_sol_leanfund = browser_leanfund_sol_leanfundContract.new(
   {
     from: web3.eth.accounts[0], 
     data: '0x6060604052341561000f57600080fd5b5b33600660006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550426009819055506000600b60006101000a81548160ff0219169083151502179055505b5b610c5c806100846000396000f300606060405236156100ef576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063058c5103146101dc57806313222d0a146101f157806338af3eed1461023e5780633c6d2e191461029357806354fd4d50146102e0578063659382fb1461030f5780637a3a0e84146103385780637b3e5e7b146103615780638da5cb5b1461038a578063be8db3cf146103df578063d8270dce14610408578063ddca3f4314610431578063f10fdf5c1461045a578063f2ec602c14610483578063fcfff16f146104ac578063fd6b7ef8146104d9578063fffb5183146104ee575b5b600a544310801561010d5750600b60009054906101000a900460ff165b156101d457670de0b6b3a76400003414151561012857600080fd5b6000600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205414156101ce5734600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540192505081905550346005600082825401925050819055505b5b6101d9565b600080fd5b5b005b34156101e757600080fd5b6101ef610539565b005b34156101fc57600080fd5b610228600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610685565b6040518082815260200191505060405180910390f35b341561024957600080fd5b61025161069d565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561029e57600080fd5b6102ca600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919050506106c2565b6040518082815260200191505060405180910390f35b34156102eb57600080fd5b6102f36106da565b604051808260ff1660ff16815260200191505060405180910390f35b341561031a57600080fd5b6103226106df565b6040518082815260200191505060405180910390f35b341561034357600080fd5b61034b6106e5565b6040518082815260200191505060405180910390f35b341561036c57600080fd5b6103746106eb565b6040518082815260200191505060405180910390f35b341561039557600080fd5b61039d6106f1565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34156103ea57600080fd5b6103f2610717565b6040518082815260200191505060405180910390f35b341561041357600080fd5b61041b61071d565b6040518082815260200191505060405180910390f35b341561043c57600080fd5b610444610723565b6040518082815260200191505060405180910390f35b341561046557600080fd5b61046d610729565b6040518082815260200191505060405180910390f35b341561048e57600080fd5b610496610771565b6040518082815260200191505060405180910390f35b34156104b757600080fd5b6104bf610777565b604051808215151515815260200191505060405180910390f35b34156104e457600080fd5b6104ec61078a565b005b34156104f957600080fd5b610537600480803590602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091908035906020019091905050610aff565b005b6000600a5443101580156105595750600b60009054906101000a900460ff165b1561067c57600660009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161480156105d357506005543073ffffffffffffffffffffffffffffffffffffffff1631115b15610676576105fb3073ffffffffffffffffffffffffffffffffffffffff1631600554610c11565b9050600660009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f1935050505015610675576000600b60006101000a81548160ff0219169083151502179055505b5b5b610681565b600080fd5b5b50565b60026020528060005260406000206000915090505481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60016020528060005260406000206000915090505481565b600181565b60045481565b60035481565b60055481565b600660009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b600a5481565b60095481565b60075481565b6000600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490505b90565b60085481565b600b60009054906101000a900460ff1681565b6000600a5443101580156107aa5750600b60009054906101000a900460ff165b15610af6576000905060035460055410801561080557506000600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054145b156109a857600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905080600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055506000600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055503373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f1935050505015156109a3576000600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208190555080600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055505b610af0565b60006004541415610a3d576109c1600554600754610c11565b9050806004600082825401925050819055506000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f193505050501515610a385760006004819055505b610aef565b600660009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16148015610a9c57506000600854145b15610aee57600754600860008282540192505081905550600660009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16ff5b5b5b5b610afb565b600080fd5b5b50565b600b60009054906101000a900460ff1680610b685750600660009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614155b15610b7257600080fd5b60f04301811015610b8257600080fd5b816000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555060006004819055506000600581905550600060078190555060006008819055508260038190555080600a819055506001600b60006101000a81548160ff0219169083151502179055505b505050565b600080828410151515610c2057fe5b82840390508091505b50929150505600a165627a7a723058205df4126a4f818afa921623ac7b7f2c98f5ab84a2a7ef2d3a837072185c8a9d510029', 
     gas: '3673062'
   }, function (e, contract){
    console.log(e, contract);
    if (typeof contract.address !== 'undefined') {
         console.log('Contract mined! address: ' + contract.address + ' transactionHash: ' + contract.transactionHash);
    }
 })

In response, you'll see:

null [object Object]
undefined
>

You can verify that the bytecode in the data field matches the file in outputs/ZcashEscrow.bin

In the geth node trace, you'll see the contract submitted for creation.

Meanwhile, back in the geth console, you'll see the contract being mined after one or two or maybe three blocks.

Contract mined! address: 0xb4309ec9549e3cb49fb83437f6c8f19ad110df64 transactionHash: 0x4445be6266ca9467a0f40a2fc63e6df3f8624a6def47829def49f6513efee663

The value 0xb4309ec9549e3cb49fb83437f6c8f19ad110df64 is known as the contract address, and yours will be different. However, we'll use ours in our example below, and you should substitute your own when carrying out these steps. Congrats! Your contract is now on the blockchain. You can view our contract here.

https://rinkeby.etherscan.io/address/0xb4309ec9549e3cb49fb83437f6c8f19ad110df64

Initializing the Contract

Now we need to specify the beneficiary, our funding goal (in wei), and our ending block deadline.

First, create the ABI.

abi = JSON.parse('[{"constant":false,"inputs":[],"name":"safeKill","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"payoutsETH","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"beneficiary","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"contributionsETH","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"version","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"payoutETH","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"fundingGoal","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"amountRaised","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"deadlineBlockNumber","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"creationTime","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"fee","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"getContribution","outputs":[{"name":"retVal","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"feeWithdrawn","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"open","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"safeWithdrawal","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_fundingGoalInWei","type":"uint256"},{"name":"_beneficiary","type":"address"},{"name":"_deadlineBlockNumber","type":"uint256"}],"name":"initialize","outputs":[],"payable":false,"type":"function"},{"inputs":[],"payable":false,"type":"constructor"},{"payable":true,"type":"fallback"}]')

in response, you'll get the ABI as a JSON object.

[{
    constant: false,
    inputs: [],
    name: "safeKill",
    outputs: [],
    payable: false,
    type: "function"
}, {
    constant: true,
    inputs: [{
        name: "",
        type: "address"
    }],
    name: "payoutsETH",
    outputs: [{
        name: "",
        type: "uint256"
    }],
    payable: false,
    type: "function"
}, {
    constant: true,
    inputs: [],
    name: "beneficiary",
    outputs: [{
        name: "",
        type: "address"
    }],
    payable: false,
    type: "function"
}, {
    constant: true,
    inputs: [{
        name: "",
        type: "address"
    }],
    name: "contributionsETH",
    outputs: [{
        name: "",
        type: "uint256"
    }],
    payable: false,
    type: "function"
}, {
    constant: true,
    inputs: [],
    name: "version",
    outputs: [{
        name: "",
        type: "uint8"
    }],
    payable: false,
    type: "function"
}, {
    constant: true,
    inputs: [],
    name: "payoutETH",
    outputs: [{
        name: "",
        type: "uint256"
    }],
    payable: false,
    type: "function"
}, {
    constant: true,
    inputs: [],
    name: "fundingGoal",
    outputs: [{
        name: "",
        type: "uint256"
    }],
    payable: false,
    type: "function"
}, {
    constant: true,
    inputs: [],
    name: "amountRaised",
    outputs: [{
        name: "",
        type: "uint256"
    }],
    payable: false,
    type: "function"
}, {
    constant: true,
    inputs: [],
    name: "owner",
    outputs: [{
        name: "",
        type: "address"
    }],
    payable: false,
    type: "function"
}, {
    constant: true,
    inputs: [],
    name: "deadlineBlockNumber",
    outputs: [{
        name: "",
        type: "uint256"
    }],
    payable: false,
    type: "function"
}, {
    constant: true,
    inputs: [],
    name: "creationTime",
    outputs: [{
        name: "",
        type: "uint256"
    }],
    payable: false,
    type: "function"
}, {
    constant: true,
    inputs: [],
    name: "fee",
    outputs: [{
        name: "",
        type: "uint256"
    }],
    payable: false,
    type: "function"
}, {
    constant: true,
    inputs: [],
    name: "getContribution",
    outputs: [{
        name: "retVal",
        type: "uint256"
    }],
    payable: false,
    type: "function"
}, {
    constant: true,
    inputs: [],
    name: "feeWithdrawn",
    outputs: [{
        name: "",
        type: "uint256"
    }],
    payable: false,
    type: "function"
}, {
    constant: true,
    inputs: [],
    name: "open",
    outputs: [{
        name: "",
        type: "bool"
    }],
    payable: false,
    type: "function"
}, {
    constant: false,
    inputs: [],
    name: "safeWithdrawal",
    outputs: [],
    payable: false,
    type: "function"
}, {
    constant: false,
    inputs: [{
        name: "_fundingGoalInWei",
        type: "uint256"
    }, {
        name: "_beneficiary",
        type: "address"
    }, {
        name: "_deadlineBlockNumber",
        type: "uint256"
    }],
    name: "initialize",
    outputs: [],
    payable: false,
    type: "function"
}, {
    inputs: [],
    payable: false,
    type: "constructor"
}]

Now create the contract instance:

> instance = web3.eth.contract(abi).at("0xb4309ec9549e3cb49fb83437f6c8f19ad110df64")

and in response, you'll get an instance object, which repeats the ABI from above.

{
  abi: [{
      constant: false,
      inputs: [],
      name: "safeKill",
      outputs: [],
      payable: false,
      type: "function"
  }, {
      constant: true,
      inputs: [{...}],
      name: "payoutsETH",
      outputs: [{...}],
      payable: false,
      type: "function"
  }, {
      constant: true,
      inputs: [],
      name: "beneficiary",
      outputs: [{...}],
      payable: false,
      type: "function"
  }, {
      constant: true,
      inputs: [{...}],
      name: "contributionsETH",
      outputs: [{...}],
      payable: false,
      type: "function"
  }, {
      constant: true,
      inputs: [],
      name: "version",
      outputs: [{...}],
      payable: false,
      type: "function"
  }, {
      constant: true,
      inputs: [],
      name: "payoutETH",
      outputs: [{...}],
      payable: false,
      type: "function"
  }, {
      constant: true,
      inputs: [],
      name: "fundingGoal",
      outputs: [{...}],
      payable: false,
      type: "function"
  }, {
      constant: true,
      inputs: [],
      name: "amountRaised",
      outputs: [{...}],
      payable: false,
      type: "function"
  }, {
      constant: true,
      inputs: [],
      name: "owner",
      outputs: [{...}],
      payable: false,
      type: "function"
  }, {
      constant: true,
      inputs: [],
      name: "deadlineBlockNumber",
      outputs: [{...}],
      payable: false,
      type: "function"
  }, {
      constant: true,
      inputs: [],
      name: "creationTime",
      outputs: [{...}],
      payable: false,
      type: "function"
  }, {
      constant: true,
      inputs: [],
      name: "fee",
      outputs: [{...}],
      payable: false,
      type: "function"
  }, {
      constant: true,
      inputs: [],
      name: "getContribution",
      outputs: [{...}],
      payable: false,
      type: "function"
  }, {
      constant: true,
      inputs: [],
      name: "feeWithdrawn",
      outputs: [{...}],
      payable: false,
      type: "function"
  }, {
      constant: true,
      inputs: [],
      name: "open",
      outputs: [{...}],
      payable: false,
      type: "function"
  }, {
      constant: false,
      inputs: [],
      name: "safeWithdrawal",
      outputs: [],
      payable: false,
      type: "function"
  }, {
      constant: false,
      inputs: [{...}, {...}, {...}],
      name: "initialize",
      outputs: [],
      payable: false,
      type: "function"
  }, {
      inputs: [],
      payable: false,
      type: "constructor"
  }, {
      payable: true,
      type: "fallback"
  }],
  address: "0xb4309ec9549e3cb49fb83437f6c8f19ad110df64",
  transactionHash: null,
  allEvents: function(),
  amountRaised: function(),
  beneficiary: function(),
  contributionsETH: function(),
  creationTime: function(),
  deadlineBlockNumber: function(),
  fee: function(),
  feeWithdrawn: function(),
  fundingGoal: function(),
  getContribution: function(),
  initialize: function(),
  open: function(),
  owner: function(),
  payoutETH: function(),
  payoutsETH: function(),
  safeKill: function(),
  safeWithdrawal: function(),
  version: function()
}
>

Now, let's run the initialization command. Copy and paste the following command:

instance.initialize(
 web3.toWei(2, "ether"),
 [eth.accounts[1]],
 eth.blockNumber + 60,
 {from: eth.accounts[2], gas: 250000})
> instance.initialize(
...  web3.toWei(2, "ether"),
...  [eth.accounts[1]],
...  eth.blockNumber + 60,
...  {from: eth.accounts[2], gas: 250000})
"0x7fc7f72acd2a49efad318b92b505f72291c5b03dd650aad2d0ee86eaa13464da"

This sets the beneficiary of the crowdfund as eth.accounts[x], with a funding goal of 2 ether, with a deadline of about 15 minutes (60 blocks) from now. All from the contract owner in eth.accounts[y].

In the geth node trace you'll see the transaction get submitted.

And you can verify that the contract parameters are set by running:

node leanfund/info.js

Paying In

You can pay in from multiple accounts, as many times as you like after initializing, but before the contract deadline.

Withdrawing

After the contract is over, any address can initiate a withdrawal. Note that if the funding goal was met, the withdrawal will go to the beneficiary, unless the withdrawal is initiated by the contract owner. Then the fee will go to the contract owner. Otherwise, if the funding goal was not met, the sending address will get back any of their contributed ether. This is similar to Kickstarter's feature of getting a refund to your credit card if the campaign doesn't get funded.

So how do we do this?

Safe Withdrawal to the Beneficiary

In the geth console, type the following (where x is the index of the account you want to use)

> personal.unlockAccount(eth.accounts[x])
Unlock account: 
Passphrase:
true
> instance.safeWithdrawal({from: eth.accounts[x], gas: 100000})
"0xc8f281e96938aa454518913f34798f6e7b4821ca708d3d4d2633f76cab8e5a54"

In the geth node trace, you'll see

INFO [08-07|00:55:49] Imported new chain segment               blocks=1  txs=1  mgas=0.047 elapsed=29.704ms  mgasps=1.595    number=671352 hash=6f6b02…2d3c08
INFO [08-07|00:55:50] Submitted transaction                    fullhash=0xc8f281e96938aa454518913f34798f6e7b4821ca708d3d4d2633f76cab8e5a54 recipient=0xb61b4cc6a321bb93822767240e7e6ac57608c038
INFO [08-07|00:56:04] Imported new chain segment               blocks=1  txs=1  mgas=4.481 elapsed=5.680ms   mgasps=788.818  number=671353 hash=7763e5…40f42e

You can now verify that the balance of the account is

Safe Withdrawal of the Fee

In the geth console, type the following (where y is the index of the contract owner)

> personal.unlockAccount(eth.accounts[y])
Unlock account: 
Passphrase:
true
> instance.safeWithdrawal({from: eth.accounts[y], gas: 100000})
"0x7dbec76d74883df75dcd21555e7aa149e60bdcefad34363d54280d27fa9e7aec"

In the geth node trace, you'll see

INFO [08-07|01:03:49] Imported new chain segment               blocks=1  txs=0  mgas=0.000 elapsed=2.526ms   mgasps=0.000    number=671384 hash=f8560e…dfa76a
INFO [08-07|01:03:52] Submitted transaction                    fullhash=0x7dbec76d74883df75dcd21555e7aa149e60bdcefad34363d54280d27fa9e7aec recipient=0xb61b4cc6a321bb93822767240e7e6ac57608c038
INFO [08-07|01:04:04] Imported new chain segment               blocks=1  txs=0  mgas=0.000 elapsed=1.601ms   mgasps=0.000    number=671385 hash=e2c95e…fa764e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment