diff --git a/.gitignore b/.gitignore index 6ebc63a..e6b9666 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /dist/main.css *.sw[po] /store +.idea \ No newline at end of file diff --git a/README.md b/README.md index 3615f2c..eeb0dba 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ window.addEventListener('message', (e) => { } }); ``` + 4. Send an `'addFunds'` request ```js walletWindow.postMessage({ @@ -66,8 +67,9 @@ walletWindow.postMessage({ }, }, WALLET_URL); ``` -5. Listen for an `'addFundsResponse'` event which will include the amount transferred and the transaction signature -```js + +5. Listen for an `'addFundsResponse'` event which will include the amount transferred and the transaction signature. +```js window.addEventListener('message', (e) => { // ... switch (e.data.method) { @@ -83,3 +85,62 @@ window.addEventListener('message', (e) => { } }); ``` +6. Send `'sendCustomTransaction'` request +```js +walletWindow.postMessage({ + method: 'sendCustomTransaction', + params: { + description: "Description of transaction", + transaction: `[ + { + "keys": [ + { + "pubkey": "9dpzQrAWRJet26mFt2pt4PXGv9J3uUj7onSTBuJYXXdZ", + "isSigner": true, + "isDebitable": true + }, + ], + "programId": "11111111111111111111111111111111", + "data": "000000003c00000000000000a90000000000000038ca84e115c5fec729ff33b77202760da632a30633f95c529a4c223c3ed6142a" + }, + { + "keys": [ + { + "pubkey": "9dpzQrAWRJet26mFt2pt4PXGv9J3uUj7onSTBuJYXXdZ", + "isSigner": true, + "isDebitable": false + }, + { + "pubkey": "Sysca11Current11111111111111111111111111111", + "isSigner": false, + "isDebitable": false + } + ], + "programId": "4pgwX1zWz8NaegwTcH1YmntCdVn7qdXwZkxqgDN7P6cR", + "data": "010000000a00000000000000320000000000000000000000000000000000000000000000" + } + ]`, + network: 'https://api.beta.testnet.solana.com', + }, +}, WALLET_URL); +``` + +The `sendCustomTransaction` request accepts a transaction in JSON format containing only the `TransactionInstruction` set. Field `data` must be in HEX, `programId` and `pubkey` must be in Base58 + +7. Listen for an `'sendCustomTransactionResponse'` event which will include the amount transferred and the transaction signature. +```js +window.addEventListener('message', (e) => { + // ... + switch (e.data.method) { + case 'ready': { + // ... + break; + } + case 'sendCustomTransactionResponse': { + const {signature} = e.data.params; + // ... + break; + } + } +}); +``` diff --git a/src/wallet.js b/src/wallet.js index 7b691a4..d2a0eb8 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -312,6 +312,12 @@ SettingsModal.propTypes = { store: PropTypes.object, }; +const WalletDisplayModeEnum = Object.freeze({ + 'MainPanel': 1, + 'TokenRequest': 2, + 'SendCustomTransactionRequest': 3 +}); + export class Wallet extends React.Component { state = { messages: [], @@ -321,7 +327,6 @@ export class Wallet extends React.Component { account: null, requestMode: false, requesterOrigin: '*', - requestPending: false, requestedPublicKey: '', requestedAmount: '', recipientPublicKey: '', @@ -329,6 +334,10 @@ export class Wallet extends React.Component { recipientIdentity: null, confirmationSignature: null, transactionConfirmed: null, + unsignedTransaction: null, + formattedUnsignedTransaction: '', + unsignedTransactionDescription: '', + displayMode: WalletDisplayModeEnum.MainPanel, }; setConfirmationSignature(confirmationSignature) { @@ -414,7 +423,6 @@ export class Wallet extends React.Component { }; onAddFunds(params, origin) { - if (!params || this.state.requestPending) return; if (!params.pubkey || !params.network) { if (!params.pubkey) this.addError(`Request did not specify a public key`); if (!params.network) this.addError(`Request did not specify a network`); @@ -439,9 +447,59 @@ export class Wallet extends React.Component { this.setState({ requesterOrigin: origin, - requestPending: true, requestedAmount: `${params.amount || ''}`, requestedPublicKey: params.pubkey, + displayMode: WalletDisplayModeEnum.TokenRequest, + }); + } + + onSendCustomTransactionRequest(params, origin) { + if (!params.network || !params.transaction) { + if (!params.network) this.addError(`Request did not specify a network`); + if (!params.transaction) this.addError(`Request did not specify a transaction`); + return; + } + + let requestedNetwork; + try { + requestedNetwork = new URL(params.network).origin; + } catch (err) { + this.addError(`Request network is invalid: "${params.network}"`); + return; + } + + const walletNetwork = new URL(this.props.store.networkEntryPoint).origin; + if (requestedNetwork !== walletNetwork) { + this.props.store.setNetworkEntryPoint(requestedNetwork); + this.addWarning( + `Changed wallet network from "${walletNetwork}" to "${requestedNetwork}"`, + ); + } + + const transaction = new web3.Transaction(); + const inputs = JSON.parse(params.transaction); + + inputs.map(input => { + const converted = {}; + converted.keys = []; + converted.programId = new web3.PublicKey(input.programId); + converted.data = Buffer.from(input.data, 'hex'); + input.keys.map(key => { + converted.keys.push({ + pubkey: new web3.PublicKey(key.pubkey), + isSigner: key.isSigner, + isDebitable: key.isDebitable, + }); + }); + transaction.add(converted); + }); + + this.setState({ + requesterOrigin: origin, + unsignedTransactionDescription: params.description ? params.description : '', + unsignedTransaction: transaction, + formattedUnsignedTransaction: JSON.stringify(JSON.parse(params.transaction), null, 4), + displayMode: WalletDisplayModeEnum.SendCustomTransactionRequest, }); } @@ -457,7 +515,10 @@ export class Wallet extends React.Component { if (e.data) { switch (e.data.method) { case 'addFunds': - this.onAddFunds(e.data.params, e.origin); + this.onAddFunds(e.data.params, e.currentTarget.origin); + return true; + case 'sendCustomTransaction': + this.onSendCustomTransactionRequest(e.data.params, e.currentTarget.origin); return true; } } @@ -508,7 +569,7 @@ export class Wallet extends React.Component { sendTransaction(closeOnSuccess) { this.runModal('Sending Transaction', 'Please wait...', async () => { const amount = this.state.recipientAmount; - this.setState({requestedAmount: '', requestPending: false}); + this.setState({requestedAmount: ''}); const transaction = web3.SystemProgram.transfer( this.state.account.publicKey, new web3.PublicKey(this.state.recipientPublicKey), @@ -542,6 +603,35 @@ export class Wallet extends React.Component { }); } + signAndSendTransaction(closeOnSuccess) { + this.runModal('Sending Transaction', 'Please wait...', async () => { + let signature = ''; + try { + signature = await web3.sendAndConfirmTransaction( + this.web3sol, + this.state.unsignedTransaction, + this.state.account, + ); + } catch (err) { + // Transaction failed but fees were still taken + this.setState({ + balance: await this.web3sol.getBalance(this.state.account.publicKey), + }); + this.postWindowMessage('sendCustomTransactionResponse', {err: true}); + throw err; + } + + this.postWindowMessage('sendCustomTransactionResponse', {signature}); + if (closeOnSuccess) { + window.close(); + } else { + this.setState({ + balance: await this.web3sol.getBalance(this.state.account.publicKey), + }); + } + }); + } + confirmTransaction() { this.runModal('Confirming Transaction', 'Please wait...', async () => { const result = await this.web3sol.confirmTransaction( @@ -557,6 +647,8 @@ export class Wallet extends React.Component { return ( this.state.recipientPublicKey === null || this.state.recipientAmount === null + ) && ( + this.state.formattedUnsignedTransaction === null ); } @@ -581,13 +673,24 @@ export class Wallet extends React.Component { /> ) : null; + let panel =
; + switch (this.state.displayMode) { + case WalletDisplayModeEnum.MainPanel: + panel = this.renderMainPanel(); + break; + case WalletDisplayModeEnum.TokenRequest: + panel = this.renderTokenRequestPanel(); + break; + case WalletDisplayModeEnum.SendCustomTransactionRequest: + panel = this.renderSendCustomTransactionRequestPanel(); + break; + } + return (
{busyModal} {settingsModal} - {this.state.requestMode - ? this.renderTokenRequestPanel() - : this.renderMainPanel()} + {panel}
); } @@ -821,6 +924,41 @@ export class Wallet extends React.Component { ); } + renderSendCustomTransactionRequestPanel() { + return ( + + Send custom transaction Request + +
+ {this.state.unsignedTransactionDescription.split('\n').map((i, key) => { + return

{i}

; + })} +
+
+ + +
+

+

+
+              {this.state.formattedUnsignedTransaction}
+            
+
+
+
+ ); + } + renderSendTokensPanel() { return (