Decentralised co-operative unions with Algorand Multisignature Account
A demonstration of how to run a decentralized co-operative union using the Algornad multisignature implementation. The co-operative has a constitution detailing how the multisignature account should operate. In this example, the rules are set below:
- A certain amount of Algo (2 Algos) is deducted from each member’s account into the multisignature co-operative account.
- 60% of the co-operative funds is transfered to a randomly selected member who hasn’t been paid in previous rounds. Payments is cyclical.
- To allow control of funds, 80% of members must sign the payout transactions. Meaning you need about 80% threshold of all members.
Requirements
- Algorand Javascript SDK. This tutorial used V2 REST API endpoints (without SDK client instantiation). A V2 SDK REST API Client example can be found Here
- AlgoExporer API version 2
- Algorand standalone accounts
Background
Currently, individuals who want to operate a co-operative or group savings account have to rely on a third-party centralised agency. However, a group individuals can use the Algorand blockchain to run such a union and set rules for contributions and payments. The Algorand multisignature account feature allows the individuals to operate a group account, set rules on how it proceeds to that accounts are shared among the members. It handles security and makes it easier to control the funds in such accounts.
The tutorial demonstrates how to process transactions via HTTP request which won’t require an Algorand Node or any third-party API account. This is ideal for developers who want to build using any programming language. It also helps new developers who are not able to set up a node but want to build apps on the chain, do so. Instantiating the client will require a PureStake account or a node set up to do so. For instance, when you use let algodclient = new algosdk.Algodv2(token, server, port);
, the developer is required to have a server url, token and port. However, with the API, you are faster in building apps and most developers are familiar with APIs and can get started with Algorand without needed tokens and server ports.
Steps
1. Set up requirements
I am doing this demonstration for 5 accounts who are members of a co-operative.
NOTE. There are helper functions for payments and making HTTPS requests to the API at the end of the script
//inlude the Algorand SDK
<script src="scripts/algosdk.min.js"></script>
//set up the urls for AlgoExplorer. Change to mainnet API //endpoints
const url = "https://api.testnet.algoexplorer.io/v2/transactions/params";
const traxUrl = "https://api.testnet.algoexplorer.io/v2/transactions";
var mnemonic1 = "phone patrol faculty crop exile mom tide want debate post trophy energy collect oil hidden cactus arrow special blossom nerve picture dry spider absent wonder";
let account1 = algosdk.mnemonicToSecretKey(mnemonic1);
// console.log(account1);
var mnemonic2 = "faith roof blame swap neglect kangaroo punch area grow profit guess achieve donate fame host deal casual torch hurdle bulb paddle coconut equal about base";
let account2 = algosdk.mnemonicToSecretKey(mnemonic2);
//console.log(account2);
var mnemonic3 = "dirt weapon various romance abuse awake bonus ecology ginger follow resemble render slice portion imitate since result cheap vital peanut rail winner crunch ability lesson";
let account3 = algosdk.mnemonicToSecretKey(mnemonic3);
//console.log(account3);
var mnemonic4 = "melody swear slot rather dinosaur width tip floor laugh coil copy track flock boy enforce craft equal defy uncle pulp catch basket clap absorb curious";
let account4 = algosdk.mnemonicToSecretKey(mnemonic4);
2. Create Multisignature Account
Create the multisignature account with the rules. In this example, I set the threshold at 80% which means 80% of members must sign every transaction out of the multisig account
//Setup the parameters for the multisig corporate account. In the constitution, 80% of members must sign to issue transactions
var threshold = 3;
const mparams = {
version: 1,
threshold: threshold,
addrs: [
account1,
account2,
account3,
account4,
],
};
var multsigaddr ="CFRR63Y4HWU7WQRIDAE4BJEUXX4RDZC2CURKZUSZYWOYSPZMLN52Y2LNX4";
//console.log(account1.addr);
const mparamsAddress = {
version: 1,
threshold: threshold,
addrs: [
account1.addr,
account2.addr,
account3.addr,
account4.addr,
],
};
if(!multsigaddr){
let multsigaddr = algosdk.multisigAddress(multsigaddr);
}
3. Deduct monthly contribution from each member into multisig account
mparams.addrs.forEach(function (items, index) {
payer = items.addr;
let payNote = new Uint8Array("Transaction : " + items.addr);
var amount = 1000000; //amount to deduct from each member into the corporative multisig account
recipient = multsigaddr
//setTimeout(()=> {
makePayments (payer, recipient,amount, payNote,items);
//}, 2000);
});
4. Select threshold (signers) and the recipient of the monthly payments
var randomShuffled = mparams.addrs.sort(() => 0.5 - Math.random());
let selected = randomShuffled.slice(0, 4);
var multisigSigners = [];
selected.forEach(function (itemPay) {
multisigSigners.push(itemPay.addr);
//console.log(multisigSigners)
});
const mparamsAccount = {
version: 1,
threshold: threshold,
addrs:multisigSigners,
};
//Assuming 1 member of the savings union received their share of the payments in the last 2 months. The programme will randomly select any of the
//3 remaining members
receivingAccount = {addrs:[account3.addr,account4.addr,account2.addr],};
let selectedSigners = randomShuffled.slice(0, threshold);
const shuffled = receivingAccount.addrs.sort(() => 0.5 - Math.random());
var PayoutRecipient = shuffled.slice(0,1);
5. Process the payments from the multisig account
The transaction ID and block ID from the payments is shown below.
// getcorporative account balance
amountUrl = "https://api.testnet.algoexplorer.io/v2/accounts/"+multsigaddr;
var json_obj = JSON.parse(Get(amountUrl));
accountBal = json_obj.amount/1000000;
//pay 60% of coorperative balance to the selected member
amountToPay = Math.round(.6*accountBal);
if (amountToPay>accountBal){alert("You don't have enough funds")}
//the multisignature account needs a threshhold of 80% to sieng a transaction. Select random 4 members from the corporative
//get first and last round params
getParams = JSON.parse(Get(url));
var note= new Uint8Array(0);
var params = {
"fee": getParams.fee,
"firstRound": getParams["last-round"],
"lastRound": getParams["last-round"] + 1000,
"genesisID": getParams["genesis-id"],
"genesisHash": getParams["genesis-hash"]
};
var arr= [];
//let each account sign the transactons
selectedSigners.forEach(function (itemPay, index,array) {
let txn = algosdk.makePaymentTxnWithSuggestedParams(multsigaddr, PayoutRecipient[0], 2000000, undefined, note, params);
// console.log(txn);
// console.log(mparamsAccount)
let signedTxnMerge = algosdk.signMultisigTransaction(txn, mparamsAddress, itemPay.sk).blob
arr.push(signedTxnMerge);
if (index === array.length - 1){
let mergedTsigTxn2 = algosdk.mergeMultisigTransactions(arr);
fetch(traxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-binary',
},
body: mergedTsigTxn2,
})
.then(response => response.json())
.then(data => {
//data responses will contain the transaction ID. You save or use it for further processing.
})
.catch((error) => {
console.error('Error:', error);
});
}
});
// console.log(suggestedParams);
function Get(yourUrl){
var Httpreq = new XMLHttpRequest(); // a new request
Httpreq.open("GET",yourUrl,false);
Httpreq.send(null);
return Httpreq.responseText;
}
function makePayments (payer, recipient, amount, payNote,items){
params = JSON.parse(Get(url));
params.fee= 1000;
let suggestedParams = {
"flatFee": true,
"fee": params.fee,
"firstRound": params["last-round"],
"lastRound": params["last-round"] + 1000,
"genesisID": params["genesis-id"],
"genesisHash": params["genesis-hash"],
};
let txn = algosdk.makePaymentTxnWithSuggestedParams(payer, recipient, amount, undefined, payNote, suggestedParams);
let signedTxnFirst = txn.signTxn(items.sk);
fetch(traxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-binary',
},
body: signedTxnFirst,
})
.then(response => response.json())
.then(data => {
})
.catch((error) => {
console.error('Error:', error);
});
}
6. Needed Functions
function Get(yourUrl){
var Httpreq = new XMLHttpRequest(); // a new request
Httpreq.open("GET",yourUrl,false);
Httpreq.send(null);
return Httpreq.responseText;
}
function makePayments (payer, recipient, amount, payNote,items){
params = JSON.parse(Get(url));
params.fee= 1000;
let suggestedParams = {
"flatFee": true,
"fee": params.fee,
"firstRound": params["last-round"],
"lastRound": params["last-round"] + 1000,
"genesisID": params["genesis-id"],
"genesisHash": params["genesis-hash"],
};
let txn = algosdk.makePaymentTxnWithSuggestedParams(payer, recipient, amount, undefined, payNote, suggestedParams);
let signedTxnFirst = txn.signTxn(items.sk);
fetch(traxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-binary',
},
body: signedTxnFirst,
})
.then(response => response.json())
.then(data => {
})
.catch((error) => {
console.error('Error:', error);
});
}
7. Github repository
https://github.com/bayuobie/Group-contributions-with-Algorand