diff --git a/examples/gno.land/r/demo/escrow/escrow.gno b/examples/gno.land/r/demo/escrow/escrow.gno new file mode 100644 index 00000000000..c52a746e173 --- /dev/null +++ b/examples/gno.land/r/demo/escrow/escrow.gno @@ -0,0 +1,263 @@ +package escrow + +import ( + fmt "gno.land/p/demo/ufmt" + "gno.land/r/demo/foo20" + "gno.land/r/demo/users" + "std" + "strconv" + "strings" + "time" +) + +type Config struct { + daoAdmin string +} + +type ContractStatus uint32 + +const ( + CREATED ContractStatus = 1 + ACCEPTED ContractStatus = 2 + CANCELED ContractStatus = 3 + PAUSED ContractStatus = 4 + COMPLETED ContractStatus = 5 +) + +type Contract struct { + id uint64 + sender string + receiver string + escrowToken string // grc20 token + escrowAmount uint64 + status ContractStatus + expireAt uint64 + clientFeedback string + sellerFeedback string +} + +// GNODAO STATE +var config Config +var contracts []Contract + +// GNODAO FUNCTIONS +func UpdateConfig(daoAdmin string) { + if config.daoAdmin == "" { + config.daoAdmin = daoAdmin + return + } + caller := std.GetOrigCaller() + if config.daoAdmin != caller.String() { + panic("not allowed to update daoAdmin") + } + + config.daoAdmin = daoAdmin +} + +func CurrentRealm() string { + return std.CurrentRealm().Addr().String() +} + +func CreateContract( + receiver string, + escrowToken string, // grc20 token + escrowAmount uint64, + duration uint64, +) { + caller := std.GetOrigCaller() + if duration == 0 { + panic("invalid duration") + } + if escrowToken == "" { + panic("invalid escrow token") + } + if escrowAmount == 0 { + panic("invalid escrow amount") + } + + contractId := uint64(len(contracts)) + contracts = append(contracts, Contract{ + id: contractId, + sender: caller.String(), + receiver: receiver, + escrowToken: escrowToken, + escrowAmount: escrowAmount, + status: CREATED, + expireAt: uint64(time.Now().Unix()) + duration, + }) + foo20.TransferFrom( + users.AddressOrName(caller.String()), + users.AddressOrName(std.CurrentRealm().Addr().String()), + escrowAmount) +} + +func CancelContract(contractId uint64) { + caller := std.GetOrigCaller() + if int(contractId) >= len(contracts) { + panic("invalid contract id") + } + + contract := contracts[contractId] + if contract.status != CREATED { + panic("contract can only be cancelled at CREATED status") + } + + if contract.sender != caller.String() { + panic("not authorized to cancel the contract") + } + + contracts[contractId].status = CANCELED + + foo20.Transfer( + users.AddressOrName(contract.sender), + contract.escrowAmount) +} + +func AcceptContract(contractId uint64) { + caller := std.GetOrigCaller() + if int(contractId) >= len(contracts) { + panic("invalid contract id") + } + + contract := contracts[contractId] + if contract.status != CREATED { + panic("contract can only be accepted at CREATED status") + } + + if contract.expireAt < uint64(time.Now().Unix()) { + panic("contract already expired") + } + + if contract.receiver != caller.String() { + panic("only associated receiver is allowed to accept") + } + contracts[contractId].status = ACCEPTED +} + +func PauseContract(contractId uint64) { + caller := std.GetOrigCaller() + if int(contractId) >= len(contracts) { + panic("invalid contract id") + } + + contract := contracts[contractId] + if contract.status != ACCEPTED { + panic("contract can only be paused at ACCEPTED status") + } + + if contract.sender != caller.String() && contract.receiver != caller.String() { + panic("only contract sender or receiver can pause") + } + contracts[contractId].status = PAUSED +} + +func CompleteContract(contractId uint64) { + caller := std.GetOrigCaller() + if int(contractId) >= len(contracts) { + panic("invalid contract id") + } + + contract := contracts[contractId] + if contract.status != ACCEPTED { + panic("contract can only be completed at ACCEPTED status") + } + + if contract.sender != caller.String() { + panic("only contract sender can complete") + } + + foo20.Transfer( + users.AddressOrName(contract.receiver), + contract.escrowAmount) + contracts[contractId].status = COMPLETED +} + +func CompleteContractByDAO(contractId uint64, sellerAmount uint64) { + caller := std.GetOrigCaller() + if int(contractId) >= len(contracts) { + panic("invalid contract id") + } + + contract := contracts[contractId] + if contract.status != PAUSED { + panic("contract can only be processed by DAO at PAUSED status") + } + + if config.daoAdmin != caller.String() { + panic("only dao admin is allowed for this operation") + } + + clientAmount := contract.escrowAmount - sellerAmount + contracts[contractId].status = COMPLETED + + foo20.Transfer( + users.AddressOrName(contract.receiver), + sellerAmount) + foo20.Transfer( + users.AddressOrName(contract.sender), + clientAmount) +} + +func GiveFeedback(contractId uint64, feedback string) { + caller := std.GetOrigCaller() + if int(contractId) >= len(contracts) { + panic("invalid contract id") + } + + contract := contracts[contractId] + if contract.status != COMPLETED { + panic("feedback can only be given after complete") + } + + if contract.sender == caller.String() { + contracts[contractId].clientFeedback = feedback + } else if contract.receiver == caller.String() { + contracts[contractId].sellerFeedback = feedback + } else { + panic("only contract participants can leave feedback") + } +} + +func GetContracts(startAfter, limit uint64) []Contract { + max := uint64(len(contracts)) + if startAfter+limit < max { + max = startAfter + limit + } + return contracts[startAfter:max] +} + +func RenderConfig() string { + return fmt.Sprintf(`{ + "daoAdmin": "%s" +}`, config.daoAdmin) +} + +func RenderContract(contractId uint64) string { + if int(contractId) >= len(contracts) { + panic("invalid contract id") + } + + c := contracts[contractId] + return fmt.Sprintf(`{ + "id": %d, + "sender": "%s", + "receiver": "%s", + "escrowToken": "%s", + "escrowAmount": %d, + "status": %d, + "expireAt": %d +}`, c.id, c.sender, c.receiver, c.escrowToken, c.escrowAmount, int(c.status), c.expireAt) +} + +func RenderContracts(startAfter uint64, limit uint64) string { + contracts := GetContracts(startAfter, limit) + rendered := "[" + for index, contract := range contracts { + rendered += RenderContract(contract.id) + if index != len(contracts)-1 { + rendered += ",\n" + } + } + rendered += "]" + return rendered +} diff --git a/examples/gno.land/r/demo/escrow/escrow_public_testnet.sh b/examples/gno.land/r/demo/escrow/escrow_public_testnet.sh new file mode 100644 index 00000000000..018778e1cda --- /dev/null +++ b/examples/gno.land/r/demo/escrow/escrow_public_testnet.sh @@ -0,0 +1,168 @@ +#!/bin/sh + +gnokey add gopher +- addr: g1x2xyqca98auaw9lnat2h9ycd4lx3w0jer9vjmt + +gnokey add gopher2 +- addr: g1c5y8jpe585uezcvlmgdjmk5jt2glfw88wxa3xq + +GOPHER=g1x2xyqca98auaw9lnat2h9ycd4lx3w0jer9vjmt + +# check balance +gnokey query bank/balances/$GOPHER -remote="test3.gno.land:36657" + +gnokey maketx addpkg \ + -deposit="1ugnot" \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="test3.gno.land:36657" \ + -chainid="test3" \ + -pkgdir="./r/escrow" \ + -pkgpath="gno.land/r/demo/escrow_03" \ + gopher + +# Set config +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="test3.gno.land:36657" \ + -chainid="test3" \ + -pkgpath="gno.land/r/demo/escrow_03" \ + -func="UpdateConfig" \ + -args="g1c5y8jpe585uezcvlmgdjmk5jt2glfw88wxa3xq" \ + gopher + +# Create Contract +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="test3.gno.land:36657" \ + -chainid="test3" \ + -pkgpath="gno.land/r/demo/escrow_03" \ + -func="CreateContract" \ + -args="g1c5y8jpe585uezcvlmgdjmk5jt2glfw88wxa3xq" \ + -args="foo20" \ + -args="100" \ + -args="60" \ + gopher + +# Cancel Contract +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="test3.gno.land:36657" \ + -chainid="test3" \ + -pkgpath="gno.land/r/demo/escrow_03" \ + -func="CancelContract" \ + -args="0" \ + gopher + +# Accept Contract +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="test3.gno.land:36657" \ + -chainid="test3" \ + -pkgpath="gno.land/r/demo/escrow_03" \ + -func="AcceptContract" \ + -args="0" \ + gopher + +# Pause Contract +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="test3.gno.land:36657" \ + -chainid="test3" \ + -pkgpath="gno.land/r/demo/escrow_03" \ + -func="PauseContract" \ + -args="0" \ + gopher + +# Complete Contract +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="test3.gno.land:36657" \ + -chainid="test3" \ + -pkgpath="gno.land/r/demo/escrow_03" \ + -func="CompleteContract" \ + -args="0" \ + gopher + +# Complete Contract by DAO +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="test3.gno.land:36657" \ + -chainid="test3" \ + -pkgpath="gno.land/r/demo/escrow_03" \ + -func="CompleteContractByDAO" \ + -args="0" \ + -args="50" \ + gopher + +# Give feedback +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="test3.gno.land:36657" \ + -chainid="test3" \ + -pkgpath="gno.land/r/demo/escrow_03" \ + -func="GiveFeedback" \ + -args="0" \ + -args="Amazing work" \ + gopher + +# Query Contracts +gnokey query "vm/qeval" -data="gno.land/r/demo/escrow_03 +RenderContracts(0, 10)" -remote="test3.gno.land:36657" + +# Query contract +gnokey query "vm/qeval" -data="gno.land/r/demo/escrow_03 +RenderContract(0)" -remote="test3.gno.land:36657" + +# Query config +gnokey query "vm/qeval" -data="gno.land/r/demo/escrow_03 +RenderConfig()" -remote="test3.gno.land:36657" + + +# Get foo20 faucet +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="test3.gno.land:36657" \ + -chainid="test3" \ + -pkgpath="gno.land/r/demo/foo20" \ + -func="Faucet" \ + gopher + +# Approve tokens +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="test3.gno.land:36657" \ + -chainid="test3" \ + -pkgpath="gno.land/r/demo/foo20" \ + -func="Approve" \ + -args="g1c5y8jpe585uezcvlmgdjmk5jt2glfw88wxa3xq" \ + -args="1000" \ + gopher + +# Query balance +gnokey query "vm/qeval" -data="gno.land/r/demo/foo20 +BalanceOf(\"$GOPHER\")" -remote="test3.gno.land:36657" + +gnokey query "vm/qeval" -data="gno.land/r/demo/foo20 +Render(\"balance/$GOPHER\")" -remote="test3.gno.land:36657" diff --git a/examples/gno.land/r/demo/escrow/escrow_teritori_testnet.sh b/examples/gno.land/r/demo/escrow/escrow_teritori_testnet.sh new file mode 100644 index 00000000000..1bc45424bb2 --- /dev/null +++ b/examples/gno.land/r/demo/escrow/escrow_teritori_testnet.sh @@ -0,0 +1,184 @@ +#!/bin/sh + +gnokey add gopher +- addr: g1x2xyqca98auaw9lnat2h9ycd4lx3w0jer9vjmt + +gnokey add gopher2 +- addr: g1c5y8jpe585uezcvlmgdjmk5jt2glfw88wxa3xq + +TERITORI=g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 +GOPHER=g1x2xyqca98auaw9lnat2h9ycd4lx3w0jer9vjmt + +# check balance +gnokey query bank/balances/$GOPHER -remote="51.15.236.215:26657" + +# Send balance to gopher2 account +gnokey maketx send \ + -send="10000000ugnot" \ + -to="g1c5y8jpe585uezcvlmgdjmk5jt2glfw88wxa3xq" \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="51.15.236.215:26657" \ + -chainid="teritori-1" \ + teritori + +gnokey maketx addpkg \ + -deposit="1ugnot" \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="51.15.236.215:26657" \ + -chainid="teritori-1" \ + -pkgdir="./r/escrow" \ + -pkgpath="gno.land/r/demo/escrow_05" \ + teritori + +# Set config +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="51.15.236.215:26657" \ + -chainid="teritori-1" \ + -pkgpath="gno.land/r/demo/escrow_05" \ + -func="UpdateConfig" \ + -args="g1c5y8jpe585uezcvlmgdjmk5jt2glfw88wxa3xq" \ + teritori + +# Create Contract +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="51.15.236.215:26657" \ + -chainid="teritori-1" \ + -pkgpath="gno.land/r/demo/escrow_05" \ + -func="CreateContract" \ + -args="g1c5y8jpe585uezcvlmgdjmk5jt2glfw88wxa3xq" \ + -args="gopher20" \ + -args="100" \ + -args="60" \ + teritori + +# Cancel Contract +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="51.15.236.215:26657" \ + -chainid="teritori-1" \ + -pkgpath="gno.land/r/demo/escrow_05" \ + -func="CancelContract" \ + -args="0" \ + teritori + +# Accept Contract +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="51.15.236.215:26657" \ + -chainid="teritori-1" \ + -pkgpath="gno.land/r/demo/escrow_05" \ + -func="AcceptContract" \ + -args="1" \ + gopher2 + +# Pause Contract +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="51.15.236.215:26657" \ + -chainid="teritori-1" \ + -pkgpath="gno.land/r/demo/escrow_05" \ + -func="PauseContract" \ + -args="0" \ + teritori + +# Complete Contract +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="51.15.236.215:26657" \ + -chainid="teritori-1" \ + -pkgpath="gno.land/r/demo/escrow_05" \ + -func="CompleteContract" \ + -args="1" \ + teritori + +# Complete Contract by DAO +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="51.15.236.215:26657" \ + -chainid="teritori-1" \ + -pkgpath="gno.land/r/demo/escrow_05" \ + -func="CompleteContractByDAO" \ + -args="0" \ + -args="50" \ + teritori + +# Give feedback +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="51.15.236.215:26657" \ + -chainid="teritori-1" \ + -pkgpath="gno.land/r/demo/escrow_05" \ + -func="GiveFeedback" \ + -args="0" \ + -args="Amazing work" \ + teritori + +# Query Contracts +gnokey query "vm/qeval" -data="gno.land/r/demo/escrow_05 +RenderContracts(0, 10)" -remote="51.15.236.215:26657" + +# Query contract +gnokey query "vm/qeval" -data="gno.land/r/demo/escrow_05 +RenderContract(0)" -remote="51.15.236.215:26657" + +# Query config +gnokey query "vm/qeval" -data="gno.land/r/demo/escrow_05 +RenderConfig()" -remote="51.15.236.215:26657" + +# Query escrow address +gnokey query "vm/qeval" -data="gno.land/r/demo/escrow_05 +CurrentRealm()" -remote="51.15.236.215:26657" + + +# Get gopher20 faucet +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="51.15.236.215:26657" \ + -chainid="teritori-1" \ + -pkgpath="gno.land/r/demo/gopher20" \ + -func="Faucet" \ + teritori + +# Approve tokens +gnokey maketx call \ + -gas-fee="1ugnot" \ + -gas-wanted="5000000" \ + -broadcast="true" \ + -remote="51.15.236.215:26657" \ + -chainid="teritori-1" \ + -pkgpath="gno.land/r/demo/gopher20" \ + -func="Approve" \ + -args="g1f7p4tuu044w2qsa9m3h64ql4lrqmmjzm2f6jws" \ + -args="1000" \ + teritori + +# Query balance +gnokey query "vm/qeval" -data="gno.land/r/demo/gopher20 +BalanceOf(\"$TERITORI\")" -remote="51.15.236.215:26657" + +gnokey query "vm/qeval" -data="gno.land/r/demo/gopher20 +Render(\"balance/g1c5y8jpe585uezcvlmgdjmk5jt2glfw88wxa3xq\")" -remote="51.15.236.215:26657" diff --git a/examples/gno.land/r/demo/escrow/escrow_test.gno b/examples/gno.land/r/demo/escrow/escrow_test.gno new file mode 100644 index 00000000000..20f510ced23 --- /dev/null +++ b/examples/gno.land/r/demo/escrow/escrow_test.gno @@ -0,0 +1 @@ +package escrow diff --git a/examples/gno.land/r/demo/escrow/spec.md b/examples/gno.land/r/demo/escrow/spec.md new file mode 100644 index 00000000000..ffc309b5e65 --- /dev/null +++ b/examples/gno.land/r/demo/escrow/spec.md @@ -0,0 +1,57 @@ +# Escrow contract specification + +## Feature Description + +We’re building a decentralized version of Fiverr.com +So, we need to code an escrow system allowing: + +- freelancers to be sure that the client have the money +- the client must be sure that he can trust the freelance +- being able to call for a vote if there is a conflict between them + +## Escrow Contract Requirements + +1. a client can create an contract by specifying the freelancer's address, escrow amount, offer expire date, contract duration(optional) +2. a client can cancel/withdraw the offer unless the seller accepted +3. a seller can't accepted the expried/canceled offers +4. a clietn can't cancel/withdraw the accepted offers +5. a client can end the contract with succuess status, it will send the escrow token to the seller. +6. a client can mint a nft that represents the feedback of the seller +7. DAO admin can end the contract if there's problem reported by client or seller. admin can decide where to send the escrowed token to send(client or seller or portions to both) + +## Completed Contract flows + +```sequence +Client->Escrow: Create an offer +Client-->Escrow: Send escrow tokens +Note left of Escrow: Check tokens received +Note left of Escrow: Create a new offer_id +Seller->Escrow: Accept an offer(offer_id) +Note left of Escrow: Check the seller address +Note left of Escrow: Check the expire date +Note left of Escrow: Update the offer status +Client->Escrow: Complete the contract(offer_id, feedback) +Note left of Escrow: Update the offer status +Escrow-->Seller: Send escrowed tokens +Escrow-->Seller: Mint a nft(level: 0-5) + + +``` + +```mermaid +flowchart LR + C(Client) -- 1. Create an offer --> O + C -. 1. send tokens .-> O + subgraph Escrow + O(Contract) + O1(Contract1) + O2(Contract2) + O3(Contract3) + O4(Contract4) + end + S(Selleer) -- 2. Accept an offer --> O + C -- 3. Complete an offer --> O + O -. 4. tokens .-> S + O -. 5. NFT .-> S + +```