JavaScript library for splitting money on ethereum
moneypipe is a collection of composable building blocks (smart contracts) for playing with money on ethereum. Basically you can create a custom address that represents a group, and simply send money to it, and the funds get auto-split to the members, either synchronously (stream) or asynchronously (buffer).
Homepage GitHub Discord Twitter
Here are the factory contracts:
- goerli testnet
- ethereum mainnet
moneypipe is all about controlling the flow of money. there are currently 2 modules:
- stream
- buffer
stream lets you create a group that auto-splits and streams money to members in realtime.
buffer lets you create a group to collect money and let members withdraw their share.
<script src="https://unpkg.com/moneypipe/dist/moneypipe.js"></script>
Install package:
npm install moneypipe
and require in your code:
const Moneypipe = require('moneypipe')
stream lets you create a group that auto-splits and streams money to members in realtime.
const stream = new Moneypipe.stream({
key: privateKey, // optional
web3: web3,
network: network
})
- web3: an instantiated web3 object
- network:
"rinkeby"
or"main"
(optional. default is "main") - key: a private key string (optional. ONLY in node.js)
- stream: the instantiated stream object
Use the constructed stream object:
let { tx, address } = await stream.create({
title: title,
members: members
})
- title: the name of the stream
- members: an array of members where each member is an object made up of the attributes:
- account: user address
- value: each account's share of the pie
- tx: the resulting transaction object
- address: the deployed stream address
get all members and their shares of a contract at stream_address
Use the constructed stream object:
let members = await stream.members(stream_address)
- stream_address: the stream contract address to fetch the members from
- members: the members array where each member is an object made up of:
- account: user address
- value: each account's share of the pie
- total: the total amount
Use the constructed stream object:
let groups = await stream.groups(owner_address)
- owner_address: (optional) the user address to query all owned streams from. the current signed-in user address if not specified.
- groups: the groups array where each item is an object made up of:
- owner: the owner address
- group: the stream address
- title: the stream title
get a stream at address
Use the constructed stream object:
let group = await stream.get(stream_address)
- stream_address: the stream address.
- group:
- owner: the owner address
- group: the stream address
- title: the stream title
buffer lets you create a group to collect money and let members withdraw their share.
while stream is a "push technology", buffer is a "pull technology". The members need to withdraw funds they can claim.
This structure means:
- requires a transaction to claim: while stream doesn't need an additional transaction, in case of buffer, each member needs to make a "claim" transaction to claim their share.
- Hyper scalable: Pipe can be used for groups with small number of members because the payout is handled in realtime and requires gas. However buffer doesn't have this overhead because each user withdraws on their own and there are no loops. Therefore a buffer can scale to as many members as you want, for example thousands or millions of members.
- requires IPFS integration: stream does everything in the smart contract. buffer requires a merkle tree. The merkle tree is stored on IPFS and its cid is stored on the smart contract, so to implement a buffer, you either need to use the official moneypipe buffer interface at https://buffer.moneypipe.xyz or run your own microipfs instance.
const buffer2 = new Moneypipe.buffer2({
web3: web3,
ipfs: ipfsPinFunction,
key: key,
network: network
})
- web3: an instantiated web3 object
- ipfs: a function that stores the mekle tree JSON on IPFS and returns the CID.
- key: a private key string (ONLY in node.js)
- network:
"rinkeby"
or"main"
(optional. default is "main")
The "ipfs" function can be implemented with microipfs. Example code below:
- buffer2: the instantiated buffer2 object
In the browser:
const buffer2 = new Moneypipe.buffer2({
web3: new Web3(window.ethereum),
ipfs: async (json) => {
let cid = await fetch("https://microipfs.com/add", {
method: "post",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
object: json
})
}).then((r) => {
return r.json()
}).then((r) => {
return r.success
})
return cid
}
})
In node.js:
const { createAlchemyWeb3 } = require("@alch/alchemy-web3");
const buffer2 = new Moneypipe.buffer2({
key: privateKey,
web3: createAlchemyWeb3(API_URL),
ipfs: async (json) => {
let cid = await fetch("https://microipfs.com/add", {
method: "post",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
object: json
})
}).then((r) => {
return r.json()
}).then((r) => {
return r.success
})
return cid
}
})
let { tx, address } = await buffer2.create({
index: index,
title: title,
members: members
})
- index: the integer index of a buffer2 contract to create (0, 1, 2, 3, ...). All buffer2 contracts have deterministic addresses, calculated from the deployer's address and the
index
. It is recommended that you create buffer2 contracts starting from 0, 1, 2, and so on. - title: the name of the stream
- members: an array of members where each member is an object made up of the attributes:
- account: user address
- value: each account's share of the pie
- tx: the buffer2 creatinon transaction
- address: the contract address for the created buffer2
There are a couple of things to remember:
- You must pass the
index
attribute when creating. Basically you have an infinite number of addresses assigned to the deployer address, and for every deployment you need to pick ones that haven't already been deployed. - If you try to deploy to an already existing slot (with an existing
index
), it will throw an error, which may look something like:
err: Error: Returned error: execution reverted: ERC1167: create2 failed
To avoid this situation, you may want to:
- Store the last deployed index somewhere
- or, if you don't want to store anything, may want to run through available addresses to find the first available address that hasn't been deployed yet.
See the
find()
method example section to learn how to find the first available address and deploy
let { tx, address } = await buffer2.create({
index: 0,
title: "testing",
members: [{
account: addr1,
value: 1
}, {
account: addr2,
value: 3
}, {
account: addr3,
value: 6
}]
})
find buffer2 contract addresses based on a query.
- Note that the find() method DOES NOT use any RPC or API and therefore does not require a network connection. It's a simple function that calculates contract addresses from any creator account.
- Because
find()
returns results based on deterministic address calculation and not from some blockchain query, it can even calculate addresses you haven't even deployed yet. This means, just from your address you will know all the future buffer2 contract addresses that will be deployed from the account.
Here's the syntax:
const addresses = await buffer2.find(query)
query
: describes the condition to search forcreator
: the creator address. can be anyone's address.start
: the contract start index to filter from (within the creator's namespace)count
: the number of results to return
addresses
: an array of contract addresses that match the condition
This example returns the first 100 buffer2 contract addresses (from index 0 to index 99) for the account 0x502b2FE7Cc3488fcfF2E16158615AF87b4Ab5C41
:
const addresses = await buffer2.find({
creator: "0x502b2FE7Cc3488fcfF2E16158615AF87b4Ab5C41",
start: 0,
count: 100
})
As mentioned above, the find()
method computes addresses instead of querying it from the blockchain.
In the example above, we are querying 100 buffer2 contracts from index 0 to 99, but not all of them may be already deployed.
To check if a buffer at an index has already been deployed or not, you can try the get(address)
method for each address to see if it throws an error:
const addresses = await buffer2.find({
creator: "0x502b2FE7Cc3488fcfF2E16158615AF87b4Ab5C41",
start: 0,
count: 100
})
for(let address of addresses) {
try {
let group = await buffer2.get(address)
console.log("group exists!", group)
} catch (e) {
console.log("no buffer2 exists at address: ", address)
}
}
3. finding 100 first available addresses and scanning until you find the first one that hasn't been deployed
You can use the similar technique to find the first available address to deploy:
const addresses = await buffer2.find({
creator: "0x502b2FE7Cc3488fcfF2E16158615AF87b4Ab5C41",
start: 0,
count: 100
})
// Find the first available index
let availableIndex;
for(let i=0; i<addresses.length; i++) {
let address = addresses[i];
try {
// this will succeed until it runs into an exception (where the address doesn't exist on chain)
let group = await buffer2.get(address)
} catch (e) {
// set the "available" to the current address since this address doesn't exist on chain
availableIndex = i;
// break as soon as we find an available address
break;
}
}
// Deploy a buffer2 at the first available index
let { tx, address } = await buffer2.create({
index: availableIndex, // Create a buffer2 at "availableIndex"
title: "testing",
members: [{
account: addr1,
value: 1
}, {
account: addr2,
value: 3
}, {
account: addr3,
value: 6
}]
})
get a buffer2 at address
Use the constructed buffer2 object:
let group = await buffer2.get(buffer2_address)
- buffer2_address: the buffer2 address.
- group:
- cid:: IPFS CID at which the merkle tree is stored
- owner: the owner address
- group: the buffer2 address
- title: the buffer2 title
get all members and their shares of a contract at buffer2_address
Use the constructed stream object:
let members = await buffer2.members(buffer2_address)
- buffer2_address: the contract address of the buffer2 to fetch the members from
- members: the members array where each member is an object made up of:
- account: user address
- value: each account's share of the pie
- total: the total amount
let members = await buffer2.members("0x05A9c70d7827c936c96896Da36676E81C878BFF0")
let { tx } = await buffer2.withdraw(buffer2_address)
- buffer2_address: the address of the buffer2 contract to withdraw balance from
- tx: the withdraw transaction object
let { tx } = await buffer2.withdraw("0x05A9c70d7827c936c96896Da36676E81C878BFF0")
get the current balance of a user
let status = await buffer2.status(buffer2_address[, account])
- buffer2_address: the buffer2 address
- account: (optional) the account for which get the status. If omitted, the currently signed-in user.
returns null
if the account is not a member of the buffer2.
otherwise returns:
- status:
- withdrawn: the total withdrawn amount for the current user (in wei)
- balance: the total amount that can be withdrawn by the current user (in wei)
- balanceEth: the balance calculated in ETH
- withdrawnEth: the total withdrawn amount for the current user, calculated in ETH
const BUFFER2_ADDRESS = "0x66360Caf43A1ee1F1D0A2dc8D0246a86d9522539"
let status = await buffer2.status(BUFFER2_ADDRESS)
for(let key in status) {
console.log(key, status[key].toString())
}
Get the current user's share value in the merkle tree and its proof
let { value, proof } = await buffer2.merkleproof(buffer2_address[, user_address])
- buffer2_address: the address of the buffer2 contract
- user_address: (optional) the address of the user account to get the proof for. if omitted, the currently signed in account.
- value: the user's share value in the merkle tree
- proof: the user's merkle proof
await window.ethereum.request({ method: 'eth_requestAccounts' })
const accounts = await this.web3.eth.getAccounts()
let { value, proof } = await buffer2.merkleproof(
"0x66360Caf43A1ee1F1D0A2dc8D0246a86d9522539",
accounts[0]
)
console.log(value, proof)