Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update dApp to work with the single-asset balance model. #80

Merged
merged 24 commits into from
Jan 10, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 1 addition & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,10 @@ backed by smart contracts on Stellar.
### Dependencies

1. `soroban-cli v0.3.3`. See https://soroban.stellar.org/docs/getting-started/setup#install-the-soroban-cli
2. `docker` for Standalone and Futurenet backends.
2. `docker` (both Standalone and Futurenet backends require it).
3. `Node.js v17`
4. `Freighter wallet v2.9.1`. Download it from https://github.com/stellar/freighter/releases/tag/2.9.1 and Enable "Experimental Mode" in the settings (gear icon).

### Backend (Local Sandbox)

1. Run the backend with `soroban serve`
2. Run `./initialize.sh sandbox` to load the contracts and initialize it.
- Note: this will create a `.soroban` sub-directory, to contain the sandbox
network data.
3. Add the Sandbox custom network in Freighter
| | |
|---|---|
| Name | Sandbox |
| URL | http://localhost:8000/soroban/rpc |
| Passphrase | Local Sandbox Stellar Network ; September 2022 |
| Allow HTTP connection | Enabled |
| Switch to this network | Enabled |

### Backend (Local Standalone Network)

1. Run the backend docker container with `./quickstart.sh standalone`, and wait for it to start.
Expand Down
117 changes: 84 additions & 33 deletions components/molecules/form-pledge/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { FunctionComponent, useState } from 'react'
import { AmountInput, Button, Checkbox } from '../../atoms'
import { TransactionModal } from '../../molecules/transaction-modal'
import styles from './style.module.css'
import { getPublicKey } from "@stellar/freighter-api"
Shaptic marked this conversation as resolved.
Show resolved Hide resolved
import {
useContractValue,
useNetwork,
Expand All @@ -10,7 +11,6 @@ import {
import * as SorobanClient from 'soroban-client'
import BigNumber from 'bignumber.js'
import * as convert from '../../../convert'
import { Account } from 'soroban-client'
import { Constants } from '../../../shared/constants'
import { accountIdentifier, contractIdentifier } from '../../../shared/identifiers'
import { Spacer } from '../../atoms/spacer'
Expand Down Expand Up @@ -43,6 +43,7 @@ const FormPledge: FunctionComponent<IFormPledgeProps> = props => {
const user = accountIdentifier(
SorobanClient.StrKey.decodeEd25519PublicKey(props.account)
)

const spender = contractIdentifier(Buffer.from(props.crowdfundId, 'hex'))
const allowanceScval = useContractValue(
props.tokenId,
Expand Down Expand Up @@ -79,9 +80,7 @@ const FormPledge: FunctionComponent<IFormPledgeProps> = props => {
xdr.ScObject.scoVec([xdr.ScVal.scvSymbol('Invoker')])
)
let nonce = convert.bigNumberToI128(BigNumber(0))
const amountScVal = convert.bigNumberToI128(
parsedAmount.shiftedBy(props.decimals).decimalPlaces(0)
)
const amountScVal = convert.bigNumberToI128(parsedAmount)

try {
if (needsApproval) {
Expand All @@ -90,8 +89,11 @@ const FormPledge: FunctionComponent<IFormPledgeProps> = props => {
props.networkPassphrase,
source,
props.tokenId,
'approve',
invoker,
// We shouldn't need to track the allowance first, because this should
// be unique to the spender and will naturally decr appropriately when
// we call deposit later.
'incr_allow',
invoker, // isn't this supposed to be a Signature, but it's Obj(Vec(["Invoker"]))???
Shaptic marked this conversation as resolved.
Show resolved Hide resolved
nonce,
spender,
amountScVal
Expand Down Expand Up @@ -138,7 +140,9 @@ const FormPledge: FunctionComponent<IFormPledgeProps> = props => {
method: string,
...params: SorobanClient.xdr.ScVal[]
): SorobanClient.Transaction {
console.log("1")
const contract = new SorobanClient.Contract(contractId)
console.log("2:", arguments)
Shaptic marked this conversation as resolved.
Show resolved Hide resolved
return new SorobanClient.TransactionBuilder(source, {
// TODO: Figure out the fee
fee: '100',
Expand Down Expand Up @@ -231,40 +235,87 @@ const FormPledge: FunctionComponent<IFormPledgeProps> = props => {

const amount = BigNumber(100)

// TODO: Check and handle approval
return (
<Button
title={`Mint ${amount.decimalPlaces(decimals).toString()} ${symbol}`}
title={`Mint ${amount.decimalPlaces(7).toString()} ${symbol}`}
onClick={async () => {
setSubmitting(true)

if (!server) throw new Error("Not connected to server")

let { sequence } = await server.getAccount(Constants.TokenAdmin)
let source = new SorobanClient.Account(Constants.TokenAdmin, sequence)
let invoker = xdr.ScVal.scvObject(
xdr.ScObject.scoVec([xdr.ScVal.scvSymbol('Invoker')])
)
let nonce = convert.bigNumberToI128(BigNumber(0))
const recipient = accountIdentifier(
SorobanClient.StrKey.decodeEd25519PublicKey(account)
)
const amountScVal = convert.bigNumberToI128(
amount.shiftedBy(decimals).decimalPlaces(0)
)
let mint = contractTransaction(
networkPassphrase,
source,
props.tokenId,
'mint',
invoker,
nonce,
recipient,
amountScVal
)
let result = await sendTransaction(mint, { secretKey: Constants.TokenAdminSecretKey })
// TODO: Show some user feedback while we are awaiting, and then based on the result
console.debug(result)
let { sequence, balances } = await server.getAccount(Constants.TokenAdmin)
let adminSource = new SorobanClient.Account(Constants.TokenAdmin, sequence)

let wallet = await getPublicKey().then((pk) => server.getAccount(pk))
let walletSource = new SorobanClient.Account(wallet.id, wallet.sequence)

//
// 1. Establish a trustline to the admin (if necessary)
// 2. The admin sends us money (mint)
//
// We have to do this in two separate transactions because one
// requires approval from Freighter while the other can be done with
// the stored token issuer's secret key.
//
// FIXME: The `getAccount()` RPC endpoint doesn't return balance
// information, so we never know whether or not the user needs
// a trustline to receive the minted asset.
//
// if (!balances || balances.filter(b => (
if (balances?.filter(b => (
b.asset_code == symbol && b.asset_issuer == Constants.TokenAdmin
)).length === 0) {
const txResult1 = await sendTransaction(
new SorobanClient.TransactionBuilder(walletSource, {
networkPassphrase,
fee: "1000", // arbitrary
})
.setTimeout(60)
.addOperation(
SorobanClient.Operation.changeTrust({
asset: new SorobanClient.Asset(symbol, Constants.TokenAdmin),
})
)
.build(), {
timeout: 60 * 1000, // should be enough time to approve the tx
skipAddingFootprint: true,
// omit `secretKey` to have Freighter prompt for signing
}
).catch((err) => {
Shaptic marked this conversation as resolved.
Show resolved Hide resolved
console.error(err)
setSubmitting(false)
})
console.debug(txResult1)
}

const txResult2 = await sendTransaction(
Shaptic marked this conversation as resolved.
Show resolved Hide resolved
new SorobanClient.TransactionBuilder(adminSource, {
networkPassphrase,
fee: "1000",
})
.setTimeout(10)
.addOperation(
SorobanClient.Operation.payment({
destination: wallet.id,
asset: new SorobanClient.Asset(symbol, Constants.TokenAdmin),
amount: amount.toString(),
})
)
.build(), {
timeout: 10 * 1000,
skipAddingFootprint: true,
secretKey: Constants.TokenAdminSecretKey,
}
).catch((err) => {
console.error(err)
setSubmitting(false)
})
console.debug(txResult2)

//
// TODO: Show some user feedback while we are awaiting, and then based
// on the result
//
setSubmitting(false)
}}
disabled={isSubmitting}
Expand Down
4 changes: 2 additions & 2 deletions components/organisms/pledge/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const Pledge: FunctionComponent = () => {
),
decimals: useContractValue(Constants.TokenId, 'decimals'),
name: useContractValue(Constants.TokenId, 'name'),
symbol: useContractValue(Constants.TokenId, 'symbol'),
symbol: useContractValue(Constants.TokenId, 'symbol')
}
}

Expand All @@ -51,7 +51,7 @@ const Pledge: FunctionComponent = () => {
const tokenName =
token.name.result && convert.scvalToString(token.name.result)
const tokenSymbol =
token.symbol.result && convert.scvalToString(token.symbol.result)
token.symbol.result && convert.scvalToString(token.symbol.result)?.replace("\u0000", "")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment on why the "\u0000" thing is needed, please?
cc @sisuresh

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why this is necessary. It looks like the symbol is "symbol", and ScSymbol allows 10 chars, so maybe something is filling out the last four chars and this just strips that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The symbol is an asset4 code; those get padded with null bytes, right? I think that's why this is there, but I need to root-cause where it comes from.

const deadlineDate =
deadline.result &&
new Date(
Expand Down
16 changes: 6 additions & 10 deletions initialize.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,10 @@ futurenet)
esac


echo Deploy the token contract
TOKEN_ID="$(
soroban token create \
--admin "$TOKEN_ADMIN" \
--name "Example Token" \
--symbol "EXT" \
--decimal 2
)"
echo Wrap the Stellar asset
mkdir -p .soroban
echo "$TOKEN_ID" > .soroban/token_id
TOKEN_ID=$(soroban token wrap --asset "EXT:$TOKEN_ADMIN")
echo -n "$TOKEN_ID" > .soroban/token_id

echo Build the crowdfund contract
make build
Expand All @@ -61,7 +55,7 @@ echo "$CROWDFUND_ID" > .soroban/crowdfund_id

echo "Contract deployed succesfully with ID: $CROWDFUND_ID"

echo Initialize the crowdfund contract
echo "Initialize the crowdfund contract"
deadline="$(($(date +"%s") + 86400))"
soroban invoke \
--id "$CROWDFUND_ID" \
Expand All @@ -71,3 +65,5 @@ soroban invoke \
--arg "1000000000" \
--arg "$TOKEN_ID" \
--wasm target/wasm32-unknown-unknown/release/soroban_crowdfund_contract.wasm

echo "Done"
17 changes: 13 additions & 4 deletions wallet/hooks/useSendTransaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,26 @@ export function useSendTransaction<E = Error>(defaultTxn?: Transaction, defaultO
for (let i = 0; i <= timeout; i+= sleepTime) {
await sleep(sleepTime);
try {
console.debug("tx id:", id)
const response = await server.getTransactionStatus(id);
console.debug(response)

switch (response.status) {
case "pending": {
continue;
}
case "success": {
if (response.results?.length != 1) {
throw new Error("Expected exactly one result");
}
setState('success');
return SorobanClient.xdr.ScVal.fromXDR(Buffer.from(response.results[0].xdr, 'base64'));
let results = response.results
if (!results) {
// FIXME: Return a more sensible value for classic transactions.
return SorobanClient.xdr.ScVal.scvI32(-1)
}
if (results.length > 1) {
throw new Error(`Expected exactly one result, got ${response.results}.`);
}

return SorobanClient.xdr.ScVal.fromXDR(Buffer.from(results[0].xdr, 'base64'));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is moving to @soroban-react/contract in #79, but let's do this here first, and move it over after.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, will wait this to be finished and I'll include it.

}
case "error": {
setState('error');
Expand Down