Merge pull request #1 from samepant/save-sig

Save transaction hash on successful broadcast
This commit is contained in:
Sam Panter 2021-08-16 21:12:05 -04:00 committed by GitHub
commit 35f62daf80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 4357 additions and 71 deletions

View File

@ -0,0 +1,13 @@
import StackableContainer from "../layout/StackableContainer";
import Button from "../inputs/Button";
export default ({ transactionHash }) => (
<StackableContainer lessPadding lessMargin>
This transaction has been broadcast.
<Button
href={`https://www.mintscan.io/cosmos/txs/${transactionHash}`}
label=" View on Mintscan"
></Button>
<style jsx>{``}</style>
</StackableContainer>
);

View File

@ -0,0 +1,41 @@
import StackableContainer from "../layout/StackableContainer";
export default ({ signatures, account }) => (
<StackableContainer lessPadding lessMargin>
<h2>Signatures</h2>
<StackableContainer lessPadding lessMargin lessRadius>
<p>
Once the number of required signatures have been created, this
transaction will be ready to broadcast.
</p>
</StackableContainer>
<StackableContainer lessPadding lessMargin lessRadius>
<div className="threshold">
<div className="current">{signatures.length}</div>
<div className="label divider">of</div>
<div className="required">{account.pubkey.value.threshold}</div>
<div className="label">signatures complete</div>
</div>
</StackableContainer>
<style jsx>{`
.threshold {
display: flex;
font-size: 30px;
justify-content: center;
align-items: center;
}
.threshold div {
padding: 0 5px;
}
.label {
font-size: 16px;
}
p {
margin-top: 1em;
}
p:first-child {
margin-top: 0;
}
`}</style>
</StackableContainer>
);

View File

@ -23,11 +23,6 @@ export default (props) => (
</div>
</li>
)}
<li>
<label>Status:</label>
<div>{props.tx.status || "signing in progress"}</div>
</li>
{props.tx.fee && (
<li>
<label>Gas:</label>

View File

@ -31,7 +31,7 @@ class TransactionForm extends React.Component {
const msgSend = {
fromAddress: this.props.address,
toAddress: toAddress,
amount: coins(amount * 1000000, "uatom"),
amount: coins(amount * 1000000, "stake"),
};
const msg = {
typeUrl: "/cosmos.bank.v1beta1.MsgSend",
@ -39,19 +39,18 @@ class TransactionForm extends React.Component {
};
const gasLimit = gas;
const fee = {
amount: coins(2000, "uatom"),
amount: coins(2000, "stake"),
gas: gasLimit.toString(),
};
return {
accountNumber: this.props.accountOnChain.accountNumber,
sequence: this.props.accountOnChain.sequence,
chainId: "cosmoshub-4",
chainId: "purp-chain",
msgs: [msg],
fee: fee,
memo: this.state.memo,
};
return baseTX;
};
handleCreate = async () => {
@ -62,7 +61,7 @@ class TransactionForm extends React.Component {
this.state.amount,
this.state.gas
);
console.log(tx);
const dataJSON = JSON.stringify(tx);
const res = await axios.post("/api/transaction", { dataJSON });
const { transactionID } = res.data;

View File

@ -1,6 +1,8 @@
import axios from "axios";
import { encode, decode } from "uint8-to-base64";
import React from "react";
import { SigningStargateClient } from "@cosmjs/stargate";
import { registry } from "@cosmjs/proto-signing";
import Button from "../inputs/Button";
import StackableContainer from "../layout/StackableContainer";
@ -13,6 +15,7 @@ export default class TransactionSigning extends React.Component {
transaction: this.props.transaction,
walletAccount: null,
walletError: null,
sigError: null,
};
}
@ -41,9 +44,14 @@ export default class TransactionSigning extends React.Component {
connectWallet = async () => {
try {
await window.keplr.enable("cosmoshub");
const walletAccount = await window.keplr.getKey("cosmoshub");
console.log(walletAccount);
window.keplr.defaultOptions = {
sign: {
preferNoSetMemo: true,
preferNoSetFee: true,
},
};
await window.keplr.enable("purp-chain");
const walletAccount = await window.keplr.getKey("purp-chain");
this.setState({ walletAccount });
} catch (e) {
console.log("enable err: ", e);
@ -52,14 +60,13 @@ export default class TransactionSigning extends React.Component {
signTransaction = async () => {
try {
const offlineSigner = window.getOfflineSigner("cosmoshub");
const offlineSigner = window.getOfflineSignerOnlyAmino("purp-chain");
const accounts = await offlineSigner.getAccounts();
console.log(accounts);
const signingClient = await SigningStargateClient.offline(offlineSigner);
const signerData = {
accountNumber: this.props.tx.accountNumber,
sequence: this.props.tx.sequence,
chainId: "cosmoshub",
chainId: "purp-chain",
};
const { bodyBytes, signatures } = await signingClient.sign(
this.state.walletAccount.bech32Address,
@ -68,30 +75,63 @@ export default class TransactionSigning extends React.Component {
this.props.tx.memo,
signerData
);
// save body bytes to the tx
// save/create the signature in the db
console.log(bodyBytes, signatures);
// check existing signatures
const bases64EncodedSignature = encode(signatures[0]);
const bases64EncodedBodyBytes = encode(bodyBytes);
const prevSigMatch = this.props.signatures.findIndex(
(signature) => signature.signature === bases64EncodedSignature
);
if (prevSigMatch > -1) {
this.setState({ sigError: "This account has already signed." });
} else {
const signature = {
bodyBytes: bases64EncodedBodyBytes,
signature: bases64EncodedSignature,
address: this.state.walletAccount.bech32Address,
};
const res = await axios.post(
`/api/transaction/${this.props.transactionID}/signature`,
signature
);
this.props.addSignature(res.data);
}
} catch (error) {
console.log(error);
console.log("Error creating signature:", error);
}
};
render() {
return (
<StackableContainer lessPadding>
<StackableContainer lessPadding lessMargin>
<h2>Sign this transaction</h2>
{this.state.walletAccount ? (
<Button label="Sign transaction" onClick={this.signTransaction} />
) : (
<Button label="Connect Wallet" onClick={this.connectWallet} />
)}
<h2>Current Signatures</h2>
{!this.state.signatures && (
<StackableContainer lessPadding lessMargin lessRadius>
<p>No signatures yet</p>
{this.state.sigError && (
<StackableContainer lessPadding lessRadius lessMargin>
<div className="signature-error">
<p>This account has already signed this transaction.</p>
</div>
</StackableContainer>
)}
<h2>Current Signers</h2>
<StackableContainer lessPadding lessMargin lessRadius>
{this.props.signatures.map((signature, i) => (
<StackableContainer
lessPadding
lessRadius
lessMargin
key={`${signature.address}_${i}`}
>
<p>{signature.address}</p>
</StackableContainer>
))}
{this.props.signatures.length === 0 && <p>No signatures yet</p>}
</StackableContainer>
<style jsx>{`
p {
text-align: center;
@ -103,6 +143,20 @@ export default class TransactionSigning extends React.Component {
h2:first-child {
margin-top: 0;
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
.signature-error p {
max-width: 550px;
color: red;
font-size: 16px;
line-height: 1.4;
}
.signature-error p:first-child {
margin-top: 0;
}
`}</style>
</StackableContainer>
);

View File

@ -1,14 +1,25 @@
const Button = (props) => (
<>
<button
className={props.primary ? "primary" : ""}
onClick={props.onClick}
disable={props.disable && props.disable.toString()}
>
{props.label}
</button>
{props.href ? (
<a
className={props.primary ? "primary button" : "button"}
href={props.href}
disabled={props.disabled && props.disabled.toString()}
>
{props.label}
</a>
) : (
<button
className={props.primary ? "primary button" : "button"}
onClick={props.onClick}
disabled={props.disabled && props.disabled.toString()}
>
{props.label}
</button>
)}
<style jsx>{`
button {
.button {
display: block;
border-radius: 10px;
background: rgba(255, 255, 255, 0.15);
border: none;
@ -17,6 +28,8 @@ const Button = (props) => (
color: white;
font-style: italic;
margin-top: 20px;
text-decoration: none;
text-align: center;
}
.primary {
border: 2px solid white;
@ -25,6 +38,10 @@ const Button = (props) => (
button:first-child {
margin-top: 0;
}
button:disabled {
opacity: 0.5;
cursor: initial;
}
`}</style>
</>
);

View File

@ -13,18 +13,16 @@ type SourceAddress {
type Transaction {
signatures: [Signature] @relation
dataJSON: String
bodyBytes: String
txHash: String
}
type Signature {
transaction: Transaction! @relation
dataJSON: String!
bodyBytes: String!
signature: String!
address: String!
}
type Query {
getMultisig(address: String!): Multisig
}
type Mutation {
setTransactionBody(address: String!): Multisig
}

View File

@ -90,10 +90,45 @@ const findTransactionByID = async (id) => {
query: `
query {
findTransactionByID(id: "${id}") {
_id
dataJSON
txHash
signatures {
data {
dataJSON
address
signature
bodyBytes
}
}
}
}
`,
},
});
};
/**
* Updates txHash of transaction on FaunaDB
*
* @param {string} id Faunadb resource id
* @param {string} txHash tx hash returned from broadcasting a tx
* @return Returns async function that makes a request to the faunadb graphql endpoint
*/
const updateTxHash = async (id, txHash) => {
return graphqlReq({
method: "POST",
data: {
query: `
mutation {
updateTransaction(id: ${id}, data: {txHash: "${txHash}"}) {
_id
dataJSON
txHash
signatures {
data {
address
signature
bodyBytes
}
}
}
@ -106,7 +141,8 @@ const findTransactionByID = async (id) => {
/**
* Creates signature record in faunadb
*
* @param {object} transaction The base transaction
* @param {object} signature an object with bodyBytes (string) and signature set (Uint8 Array)
* @param {string} transactionId id of the transaction to relate the signature with
* @return Returns async function that makes a request to the faunadb graphql endpoint
*/
const createSignature = async (signature, transactionId) => {
@ -117,9 +153,14 @@ const createSignature = async (signature, transactionId) => {
mutation {
createSignature(data: {
transaction: {connect: ${transactionId}},
dataJSON:
bodyBytes: "${signature.bodyBytes}",
signature: "${signature.signature}",
address: "${signature.address}"
}) {
_id
address
signature
address
}
}
`,
@ -127,4 +168,11 @@ const createSignature = async (signature, transactionId) => {
});
};
export { createMultisig, getMultisig, createTransaction, findTransactionByID };
export {
createMultisig,
getMultisig,
createTransaction,
findTransactionByID,
updateTxHash,
createSignature,
};

View File

@ -54,7 +54,7 @@ const getMultisigAccount = async (address, client) => {
// of this tool its pubkey will be available in the fauna datastore
let accountOnChain = await client.getAccount(address);
if (!accountOnChain.pubkey) {
if (!accountOnChain || !accountOnChain.pubkey) {
console.log("No pubkey on chain for: ", address);
const res = await getMultisig(address);
@ -64,6 +64,10 @@ const getMultisigAccount = async (address, client) => {
);
}
const pubkey = JSON.parse(res.data.data.getMultisig.pubkeyJSON);
if (!accountOnChain) {
accountOnChain = {};
}
accountOnChain.pubkey = pubkey;
}
return accountOnChain;

4017
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,10 +6,11 @@
"start": "next start"
},
"dependencies": {
"@cosmjs/amino": "^0.25.0-alpha.1",
"@cosmjs/launchpad": "^0.25.0-alpha.1",
"@cosmjs/proto-signing": "^0.25.0-alpha.1",
"@cosmjs/stargate": "^0.25.0-alpha.1",
"@cosmjs/amino": "^0.25.0-alpha.1",
"@keplr-wallet/types": "^0.9.0-alpha.4",
"axios": "^0.21.1",
"chalk": "^4.1.0",
"encoding": "^0.1.13",
@ -17,6 +18,7 @@
"next": "^10.0.7",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"uint8-to-base64": "^0.2.0",
"uuid": "^8.3.0"
}
}

View File

@ -0,0 +1,25 @@
import { updateTxHash } from "../../../../lib/graphqlHelpers";
export default async function (req, res) {
return new Promise(async (resolve) => {
switch (req.method) {
case "POST":
try {
const { transactionID } = req.query;
const { txHash } = req.body;
console.log("Function `updateTransaction` invoked", txHash);
const saveRes = await updateTxHash(transactionID, txHash);
console.log("success", saveRes.data);
res.status(200).send(saveRes.data.data.updateTransaction);
return resolve();
} catch (err) {
console.log(err);
res.status(400).send(err.message);
return resolve();
}
}
// no route matched
res.status(405).end();
return resolve();
});
}

View File

@ -1,17 +1,16 @@
import { createTransaction } from "../../../../lib/graphqlHelpers";
import { createSignature } from "../../../../lib/graphqlHelpers";
export default async function (req, res) {
return new Promise(async (resolve) => {
switch (req.method) {
case "POST":
try {
const { transactionID } = req.query;
const data = req.body;
console.log("Function `createTransaction` invoked", data);
const saveRes = await createTransaction(data.dataJSON);
console.log("Function `createSignature` invoked", data);
const saveRes = await createSignature(data, transactionID);
console.log("success", saveRes.data);
res
.status(200)
.send({ transactionID: saveRes.data.data.createTransaction._id });
res.status(200).send(saveRes.data.data.createSignature);
return resolve();
} catch (err) {
console.log(err);

View File

@ -14,9 +14,9 @@ import TransactionList from "../../../components/dataViews/TransactionList";
export async function getServerSideProps(context) {
let holdings;
try {
const client = await StargateClient.connect("143.198.6.14:26657");
const client = await StargateClient.connect(process.env.NODE_ADDRESS);
const multisigAddress = context.params.address;
holdings = await client.getBalance(multisigAddress, "uatom");
holdings = await client.getBalance(multisigAddress, "stake");
const accountOnChain = await getMultisigAccount(multisigAddress, client);
return {

View File

@ -1,48 +1,140 @@
import { StargateClient } from "@cosmjs/stargate";
import axios from "axios";
import { StargateClient, makeMultisignedTx } from "@cosmjs/stargate";
import { TxRaw } from "@cosmjs/stargate/build/codec/cosmos/tx/v1beta1/tx";
import { useState } from "react";
import { encode, decode } from "uint8-to-base64";
import { createMultisigThresholdPubkey, pubkeyToAddress } from "@cosmjs/amino";
import { registry } from "@cosmjs/proto-signing";
import Button from "../../../../components/inputs/Button";
import { findTransactionByID } from "../../../../lib/graphqlHelpers";
import { getMultisigAccount } from "../../../../lib/multisigHelpers";
import Page from "../../../../components/layout/Page";
import StackableContainer from "../../../../components/layout/StackableContainer";
import ThresholdInfo from "../../../../components/dataViews/ThresholdInfo";
import TransactionInfo from "../../../../components/dataViews/TransactionInfo";
import TransactionSigning from "../../../../components/forms/TransactionSigning";
import CompletedTransaction from "../../../../components/dataViews/CompletedTransaction";
export async function getServerSideProps(context) {
// get multisig account and transaction info
const client = await StargateClient.connect("143.198.6.14:26657");
const nodeAddress = process.env.NODE_ADDRESS;
const client = await StargateClient.connect(nodeAddress);
const multisigAddress = context.params.address;
const holdings = await client.getBalance(multisigAddress, "uatom");
const transactionID = context.params.transactionID;
let transactionJSON;
let txHash;
let accountOnChain;
let signatures;
try {
accountOnChain = await getMultisigAccount(multisigAddress, client);
console.log("Function `findTransactionByID` invoked", transactionID);
const getRes = await findTransactionByID(transactionID);
console.log("success", getRes.data);
txHash = getRes.data.data.findTransactionByID.txHash;
transactionJSON = getRes.data.data.findTransactionByID.dataJSON;
signatures = getRes.data.data.findTransactionByID.signatures.data || [];
} catch (err) {
console.log(err);
}
return {
props: { transactionJSON, accountOnChain, holdings },
props: {
transactionJSON,
txHash,
accountOnChain,
holdings,
transactionID,
signatures,
nodeAddress,
},
};
}
const transactionPage = ({ transactionJSON }) => {
const transactionPage = ({
transactionJSON,
transactionID,
signatures,
accountOnChain,
nodeAddress,
txHash,
}) => {
const [currentSignatures, setCurrentSignatures] = useState(signatures);
const [isBroadcasting, setIsBroadcasting] = useState(false);
const [transactionHash, setTransactionHash] = useState(txHash);
const txInfo = (transactionJSON && JSON.parse(transactionJSON)) || null;
console.log(txInfo);
const addSignature = (signature) => {
setCurrentSignatures(currentSignatures.push(signature));
};
const broadcastTx = async () => {
setIsBroadcasting(true);
const signatures = new Map();
currentSignatures.forEach((signature) => {
signatures.set(signature.address, decode(signature.signature));
});
const bodyBytes = decode(currentSignatures[0].bodyBytes);
const signedTx = makeMultisignedTx(
accountOnChain.pubkey,
txInfo.sequence,
txInfo.fee,
bodyBytes,
signatures
);
const broadcaster = await StargateClient.connect(nodeAddress);
const result = await broadcaster.broadcastTx(
Uint8Array.from(TxRaw.encode(signedTx).finish())
);
console.log(result);
const res = await axios.post(`/api/transaction/${transactionID}`, {
txHash: result.transactionHash,
});
setTransactionHash(result.transactionHash);
};
return (
<Page>
<StackableContainer base>
<StackableContainer>
<h1>In Progress Transaction</h1>
<h1>
{transactionHash
? "Completed Transaction"
: "In Progress Transaction"}
</h1>
</StackableContainer>
{transactionHash && (
<CompletedTransaction transactionHash={transactionHash} />
)}
<TransactionInfo tx={txInfo} />
<TransactionSigning tx={txInfo} />
{!transactionHash && (
<ThresholdInfo signatures={signatures} account={accountOnChain} />
)}
{signatures.length >= parseInt(accountOnChain.pubkey.value.threshold) &&
!transactionHash && (
<Button
label={
isBroadcasting ? "Broadcasting..." : "Broadcast Transaction"
}
onClick={broadcastTx}
primary
disabled={isBroadcasting}
/>
)}
{!transactionHash && (
<TransactionSigning
tx={txInfo}
transactionID={transactionID}
signatures={signatures}
addSignature={addSignature}
/>
)}
</StackableContainer>
<style jsx>{``}</style>
<style jsx>{`
.broadcast {
}
`}</style>
</Page>
);
};