diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 91e1db0b..296fc734 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,20 +2,19 @@ name: Build on: push: - branches: ['main'] + branches: ['main'] pull_request: - branches: ['*'] - + branches: ['*'] jobs: - build: - name: Build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - uses: sonarsource/sonarqube-scan-action@master - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: sonarsource/sonarqube-scan-action@master + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bf0b2a0e..6df9b235 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -51,6 +51,7 @@ jobs: VITE_HORIZON_NETWORK_PASSPHRASE: Test SDF Network ; September 2015 VITE_STELLAR_NETWORK: ${{ secrets.VITE_STELLAR_NETWORK }} CYPRESS_SIMPLE_SIGNER_PRIVATE_KEY: ${{ secrets.SIMPLE_SIGNER_PRIVATE_KEY }} + VITE_HORIZON_URL: ${{ secrets.VITE_HORIZON_URL }} test: runs-on: ubuntu-latest needs: install-cache diff --git a/README.md b/README.md index 6c7c649d..18ab2545 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ you want to contribute. # How to implement Simple Signer on your website -Simple Signer provides two endpoints, `/connect` and `/sign` which allow some customisations to be made. +Simple Signer provides three endpoints, `/connect`, `/sign` and `/payment` which allow some customisations to be made. To see an example of all the implementation properties please take a look at the [test.html](./test.html) file provided in this repo. @@ -329,6 +329,118 @@ Sometimes it's useful to group operations together to explain what they are doin --- +## Making a payment + +To make a payment using Simple Signer, you need to provide the necessary parameters either through the URL or using the `postMessage` method. Follow the steps below to integrate the payment functionality into your web application: + +### Step 1: Specify Payment Parameters + +You can pass the payment parameters such as the receiver's account, amount, asset type, and issuer through the URL or `postMessage` method. Here's an example of how you can do it: + +```html + + + + + + Simple Signer - Make Payment Demo + + + + +

Make Payment

+

+ This page demonstrates the process of making a payment using the + Simple Signer. +

+

Usage

+

Click the "Make Payment" button to initiate the payment process.

+ + + + + +``` + +You may choose to pass the payment parameters to Simple Signer either via URL or via postMessage. + +Via URL: + +```javascript +const receiver = 'Receiver public key'; +const amount = '10'; +const assetCode = 'native'; // 'native' for XLM, or asset code for other assets +const issuer = ''; // If assetCode is not 'native', provide issuer's public key + +const paymentWindow = window.open( + `https://sign.scalemote.io/payment/?receiver=${receiver}&amount=${amount}&assetCode=${assetCode}&issuer=${issuer}`, + 'Payment_Window', + 'width=360, height=700', +); +``` + +Via PostMessage: + +Post Message has some advantages over the URL method which are covered in the Payment API section. + +```javascript +const receiver = 'Receiver public key'; +const amount = '10'; +const assetCode = 'native'; // 'native' for XLM, or asset code for other assets +const issuer = ''; // If assetCode is not 'native', provide issuer's public key + +const simpleSignerUrl = 'https://sign.scalemote.io'; +const paymentWindow = window.open( + `${simpleSignerUrl}/payment`, + 'Payment_Window', + 'width=360, height=700', +); + +window.addEventListener('message', (e) => { + if ( + e.origin !== simpleSignerUrl && + e.data.type === 'onReady' && + e.data.page === 'payment' + ) { + paymentWindow.postMessage( + { receiver, amount, assetCode, issuer }, + simpleSignerUrl, + ); + } +}); +``` + ## Language selection By default, Simple Signer will detect the browser's language and serve Simple Signer using this configuration. If the diff --git a/cypress/fixtures/payment.json b/cypress/fixtures/payment.json new file mode 100644 index 00000000..5b698c57 --- /dev/null +++ b/cypress/fixtures/payment.json @@ -0,0 +1,6 @@ +{ + "destinationAccount": "GCECUYXE32PPYKPVYOSILACOCUGMEZSDO7GYERC2GKAG2HM34BLMMOMB", + "amountToSend": 100, + "assetCode": "native", + "issuer": "none" +} diff --git a/cypress/integration/ui/events_spec.ts b/cypress/integration/ui/events_spec.ts index 00c43995..2fcb8263 100644 --- a/cypress/integration/ui/events_spec.ts +++ b/cypress/integration/ui/events_spec.ts @@ -1,6 +1,7 @@ /// /// import { operationsXdr } from '../../fixtures/operations.json'; +import { amountToSend, assetCode, destinationAccount, issuer } from '../../fixtures/payment.json'; const operationGroupTitle = 'Payment'; const operationGroupDescription = 'This is a merge account operation'; @@ -37,4 +38,17 @@ describe('Events', () => { cy.get('.tx-operation-container').should('have.length', 2); cy.get('.tx-description-container').contains(operationDescription); }); + + it('should render a payment operation', () => { + cy.visit('/payment'); + cy.window().then((win) => { + win.postMessage({ + receiver: destinationAccount, + issuer, + amount: amountToSend, + assetCode, + }); + }); + cy.get('.receiver').should('have.length', 1); + }); }); diff --git a/cypress/integration/ui/logout_spec.ts b/cypress/integration/ui/logout_spec.ts index 73d9bc08..05714cbd 100644 --- a/cypress/integration/ui/logout_spec.ts +++ b/cypress/integration/ui/logout_spec.ts @@ -22,4 +22,13 @@ describe('logout', () => { cy.get('.logout-active').contains('Logout').click(); cy.url().should('include', '/connect'); }); + + it('Should logout on /payment', () => { + cy.visit('/payment'); + window.localStorage.setItem('wallet', 'xbull'); + cy.wait(5000); + cy.get('.logout-button').click(); + cy.get('.logout-active').contains('Logout').click(); + cy.url().should('include', '/connect'); + }); }); diff --git a/cypress/integration/ui/payment_spec.ts b/cypress/integration/ui/payment_spec.ts new file mode 100644 index 00000000..fa1423e0 --- /dev/null +++ b/cypress/integration/ui/payment_spec.ts @@ -0,0 +1,29 @@ +/// +/// +import { amountToSend, assetCode, destinationAccount, issuer } from '../../fixtures/payment.json'; + +describe('checks that the /payment component works', () => { + const BASE_URL = '/payment'; + + it('should visit /payment with payment information but user is not connected', () => { + cy.visit( + `${BASE_URL}?receiver=${destinationAccount}&amount=${amountToSend}&assetCode=${assetCode}&issuer=${issuer}`, + ); + cy.get('.user-not-connected').contains('User is not connected'); + cy.get('.payment-btn').click(); + cy.url().should('include', '/connect'); + }); + + it('should render payment information if payment parameters are valid', () => { + window.localStorage.setItem('wallet', 'xbull'); + cy.visit( + `${BASE_URL}?receiver=${destinationAccount}&amount=${amountToSend}&assetCode=${assetCode}&issuer=${issuer}`, + ); + cy.get('.simple-signer').contains(`You are paying ${amountToSend} XLM to the account ${destinationAccount}.`); + }); + + it('should render an error if payment parameters was not provided', () => { + cy.visit(`${BASE_URL}?receiver=${destinationAccount}`); + cy.get('.simple-signer').contains("Sorry, the recipient's data wasn't provided."); + }); +}); diff --git a/package-lock.json b/package-lock.json index 43248c9f..372ccf9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1739,14 +1739,6 @@ "sodium-native": "^4.0.1" } }, - "node_modules/@stellar/stellar-base/node_modules/bignumber.js": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", - "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", - "engines": { - "node": "*" - } - }, "node_modules/@stellar/stellar-base/node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -1770,16 +1762,6 @@ "ieee754": "^1.2.1" } }, - "node_modules/@stellar/stellar-base/node_modules/sodium-native": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.0.4.tgz", - "integrity": "sha512-faqOKw4WQKK7r/ybn6Lqo1F9+L5T6NlBJJYvpxbZPetpWylUVqz449mvlwIBKBqxEHbWakWuOlUt8J3Qpc4sWw==", - "hasInstallScript": true, - "optional": true, - "dependencies": { - "node-gyp-build": "^4.6.0" - } - }, "node_modules/@sveltejs/vite-plugin-svelte": { "version": "1.0.0-next.44", "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.0.0-next.44.tgz", @@ -3155,6 +3137,34 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, + "node_modules/axios": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/axios/node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/babel-jest": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", @@ -3304,6 +3314,14 @@ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", "dev": true }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -9341,6 +9359,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/sodium-native": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.0.6.tgz", + "integrity": "sha512-uYsyycwcz9kYDwpXxJmL2YZosynsxcP6RPySbARVJdC9uNDa2CMjzJ7/WsMMvThKgvAYsBWdZc7L/WSVj9lTcA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build": "^4.6.0" + } + }, "node_modules/sonic-boom": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz", @@ -9506,42 +9534,6 @@ "urijs": "^1.19.1" } }, - "node_modules/stellar-sdk/node_modules/axios": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.4.tgz", - "integrity": "sha512-heJnIs6N4aa1eSthhN9M5ioILu8Wi8vmQW9iHQ9NUvfkJb0lEEDUiIdQNAuBtfUt3FxReaKdpQA5DbmMOqzF/A==", - "dependencies": { - "follow-redirects": "^1.15.4", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/stellar-sdk/node_modules/bignumber.js": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", - "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", - "engines": { - "node": "*" - } - }, - "node_modules/stellar-sdk/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/stellar-sdk/node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "node_modules/stream-shift": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", @@ -12076,11 +12068,6 @@ "tweetnacl": "^1.0.3" }, "dependencies": { - "bignumber.js": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", - "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==" - }, "buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -12089,15 +12076,6 @@ "base64-js": "^1.3.1", "ieee754": "^1.2.1" } - }, - "sodium-native": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.0.4.tgz", - "integrity": "sha512-faqOKw4WQKK7r/ybn6Lqo1F9+L5T6NlBJJYvpxbZPetpWylUVqz449mvlwIBKBqxEHbWakWuOlUt8J3Qpc4sWw==", - "optional": true, - "requires": { - "node-gyp-build": "^4.6.0" - } } } }, @@ -13220,6 +13198,33 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, + "axios": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "requires": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + } + } + }, "babel-jest": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", @@ -13330,6 +13335,11 @@ } } }, + "bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==" + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -17754,6 +17764,15 @@ } } }, + "sodium-native": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.0.6.tgz", + "integrity": "sha512-uYsyycwcz9kYDwpXxJmL2YZosynsxcP6RPySbARVJdC9uNDa2CMjzJ7/WsMMvThKgvAYsBWdZc7L/WSVj9lTcA==", + "optional": true, + "requires": { + "node-gyp-build": "^4.6.0" + } + }, "sonic-boom": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz", @@ -17890,38 +17909,6 @@ "randombytes": "^2.1.0", "toml": "^3.0.0", "urijs": "^1.19.1" - }, - "dependencies": { - "axios": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.4.tgz", - "integrity": "sha512-heJnIs6N4aa1eSthhN9M5ioILu8Wi8vmQW9iHQ9NUvfkJb0lEEDUiIdQNAuBtfUt3FxReaKdpQA5DbmMOqzF/A==", - "requires": { - "follow-redirects": "^1.15.4", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "bignumber.js": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", - "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==" - }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - } } }, "stream-shift": { diff --git a/src/App.svelte b/src/App.svelte index 42c15fc7..557a8b12 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -6,6 +6,7 @@ import { WalletConnectService } from './lib/service/walletConnect'; import Home from './routes/Home.svelte'; import Connect from './routes/connect/Connect.svelte'; + import Payment from './routes/payment/Payment.svelte'; import Sign from './routes/sign/Sign.svelte'; import { detectedLanguage, isLanguageLoading, walletConnectClient } from './store/global'; @@ -28,6 +29,7 @@ + {/if} diff --git a/src/constants.ts b/src/constants.ts index c845590d..94d6c471 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -3,6 +3,7 @@ const { VITE_DAPP_BASE_URL: DAPP_BASE_URL, VITE_HORIZON_NETWORK_PASSPHRASE: HORIZON_NETWORK_PASSPHRASE, VITE_STELLAR_NETWORK: STELLAR_NETWORK, + VITE_HORIZON_URL: HORIZON_URL, } = import.meta.env; -export { PROJECT_ID_FOR_WALLET_CONNECT, DAPP_BASE_URL, HORIZON_NETWORK_PASSPHRASE, STELLAR_NETWORK }; +export { PROJECT_ID_FOR_WALLET_CONNECT, DAPP_BASE_URL, HORIZON_NETWORK_PASSPHRASE, STELLAR_NETWORK, HORIZON_URL }; diff --git a/src/env.d.ts b/src/env.d.ts index bc9bb645..daf5b676 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -5,6 +5,7 @@ interface ImportMetaEnv { readonly VITE_STELLAR_NETWORK: string; readonly VITE_PROJECT_ID_FOR_WALLET_CONNECT: string; readonly VITE_DAPP_BASE_URL: string; + readonly VITE_HORIZON_URL: string; } interface ImportMeta { diff --git a/src/lib/bridge/Bridge.ts b/src/lib/bridge/Bridge.ts index 17311ce4..3a11a009 100644 --- a/src/lib/bridge/Bridge.ts +++ b/src/lib/bridge/Bridge.ts @@ -1,21 +1,25 @@ import EventFactory from './EventFactory'; import type ISimpleSignerEvent from './ISimpleSignerEvent'; import type IAvailableWalletsMessage from './availableWalletsMessage/IAvailableWalletsMessage'; +import type { IPaymentMessage } from './paymentMessage/IPaymentMessage'; import type { ITransactionMessage } from './transactionMessage/ITransactionMessage'; export type IAvailableWalletsMessageHandler = (message: IAvailableWalletsMessage) => void; export type ITransactionMessageHandler = (message: ITransactionMessage) => void; +export type IPaymentMessageHandler = (message: IPaymentMessage) => void; export enum SimpleSignerEventType { ON_CONNECT = 'onConnect', ON_READY = 'onReady', ON_SIGN = 'onSign', ON_CANCEL = 'onCancel', + ON_PAYMENT = 'onPayment', } export enum SimpleSignerPageType { CONNECT = 'connect', SIGN = 'sign', + PAYMENT = 'payment', } export default class Bridge { @@ -31,6 +35,7 @@ export default class Bridge { } private availableWalletsMessageHandlers: IAvailableWalletsMessageHandler[] = []; private transactionMessageHandlers: ITransactionMessageHandler[] = []; + private paymentMessageHandlers: IPaymentMessageHandler[] = []; public sendSignedTx(signedXDR: string) { this.mainActionPerformed = true; @@ -49,7 +54,6 @@ export default class Bridge { public sendOnConnectEvent(publicKey: string, wallet: string): void { this.mainActionPerformed = true; this.sendMessage(EventFactory.createOnConnectEvent(publicKey, wallet)); - this.closeWindow(); } public addAvailableWalletsMessageHandler(handler: IAvailableWalletsMessageHandler) { @@ -60,6 +64,10 @@ export default class Bridge { this.transactionMessageHandlers.push(handler); } + public addPaymentMessageHandler(handler: IPaymentMessageHandler) { + this.paymentMessageHandlers.push(handler); + } + public getTransactionMessageFromUrl(queryString?: string): ITransactionMessage | null { const urlParams = new URLSearchParams(queryString || window.location.search); const xdrParam = urlParams.get('xdr'); @@ -77,12 +85,37 @@ export default class Bridge { } } + public getPaymentMessageFromUrl(queryString?: string): IPaymentMessage | null { + const urlParams = new URLSearchParams(queryString || window.location.search); + const receiver = urlParams.get('receiver'); + const amount = urlParams.get('amount'); + const assetCode = urlParams.get('assetCode'); + const issuer = urlParams.get('issuer'); + + if (receiver && amount && assetCode && issuer) { + return { + receiver, + amount, + assetCode, + issuer, + }; + } else { + return null; + } + } + public getWalletsFromUrl(): string[] { const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); return urlParams.getAll('wallets'); } + public getRedirectFromUrl(): string | null { + const queryString = window.location.search; + const urlParams = new URLSearchParams(queryString); + return urlParams.get('redirect'); + } + private messageHandler(e: MessageEvent): void { if ('wallets' in e.data) { const message = e.data as IAvailableWalletsMessage; @@ -95,9 +128,15 @@ export default class Bridge { this.transactionMessageHandlers.forEach((handler) => handler(message)); return; } + + if ('receiver' in e.data && 'amount' in e.data && 'assetCode' in e.data && 'issuer' in e.data) { + const message = e.data as IPaymentMessage; + this.paymentMessageHandlers.forEach((handler) => handler(message)); + return; + } } - private closeWindow() { + public closeWindow() { return window.close(); } diff --git a/src/lib/bridge/EventFactory.ts b/src/lib/bridge/EventFactory.ts index c9de5c77..8925d63f 100644 --- a/src/lib/bridge/EventFactory.ts +++ b/src/lib/bridge/EventFactory.ts @@ -38,4 +38,22 @@ export default class EventFactory { page: SimpleSignerPageType.SIGN, }; } + + static createOnPaymentRequest( + receiver: string, + amount: number, + assetCode: string, + issuer: string, + ): ISimpleSignerEvent { + return { + type: SimpleSignerEventType.ON_PAYMENT, + message: { + receiver, + amount, + assetCode, + issuer, + }, + page: SimpleSignerPageType.PAYMENT, + }; + } } diff --git a/src/lib/bridge/paymentMessage/IPaymentMessage.ts b/src/lib/bridge/paymentMessage/IPaymentMessage.ts new file mode 100644 index 00000000..592d9ca3 --- /dev/null +++ b/src/lib/bridge/paymentMessage/IPaymentMessage.ts @@ -0,0 +1,6 @@ +export interface IPaymentMessage { + receiver: string; + amount: string; + assetCode: string; + issuer: string; +} diff --git a/src/lib/i18n/ITranslation.ts b/src/lib/i18n/ITranslation.ts index 1757752c..34482f11 100644 --- a/src/lib/i18n/ITranslation.ts +++ b/src/lib/i18n/ITranslation.ts @@ -30,8 +30,10 @@ export interface ITranslation { ENGLISH_ISO: string; ENGLISH: string; ERROR: string; + ERROR_MISSING_RECEIVER_DATA: string; EXPAND_ALL: string; EXTEND_TO: string; + FAILED_PAYMENT: string; FEE_BUMP_DESCRIPTION_1: string; FEE_BUMP_DESCRIPTION_2: string; FEE_BUMP: string; @@ -40,6 +42,7 @@ export interface ITranslation { FUNCTION_TYPE: string; GO_TO_CONNECT: string; GO_TO_SIGN: string; + GO_TO_PAYMENT: string; HIDE_ALL: string; HIDE_KEY: string; HIGH_THRESHOLD: string; @@ -108,6 +111,7 @@ export interface ITranslation { OPERATION: string; OPERATIONS_LIST: string; PATH: string; + PAY: string; PREAUTH_TX: string; PRICE: string; PRIVATE_KEY: string; @@ -129,7 +133,9 @@ export interface ITranslation { SPANISH: string; SPONSORED_ID: string; STARTING_BALANCE: string; + SUCCESSFUL_PAYMENT: string; TIME_BOUNDS: string; + TO_THE_ACCOUNT: string; TRANSACTION: string; TRUSTOR: string; USER_IS_NOT_CONNECTED: string; @@ -137,5 +143,6 @@ export interface ITranslation { WEIGHT: string; XDR_INVALID: string; XDR_NOT_PROVIDED: string; + YOU_ARE_PAYING: string; YOUR_ACCOUNT: string; } diff --git a/src/lib/i18n/languages/english.json b/src/lib/i18n/languages/english.json index b3cde2cb..c86035d9 100644 --- a/src/lib/i18n/languages/english.json +++ b/src/lib/i18n/languages/english.json @@ -30,8 +30,10 @@ "ENGLISH_ISO": "en", "ENGLISH": "English", "ERROR": "Error", + "ERROR_MISSING_RECEIVER_DATA": "Sorry, the recipient's data wasn't provided.", "EXPAND_ALL": "Expand all", "EXTEND_TO": "Extend to:", + "FAILED_PAYMENT": "Payment failed. Please try again.", "FEE_BUMP_DESCRIPTION_1": "You will sign to pay the", "FEE_BUMP_DESCRIPTION_2": "of the transaction below", "FEE_BUMP": "FEE BUMP", @@ -40,6 +42,7 @@ "FUNCTION_TYPE": "Function type:", "GO_TO_CONNECT": "Go to Connect", "GO_TO_SIGN": "Go to Sign", + "GO_TO_PAYMENT": "Go to Payment", "HIDE_ALL": "Hide all", "HIDE_KEY": "Hide key", "HIGH_THRESHOLD": "High Threshold:", @@ -108,6 +111,7 @@ "OPERATION": "Operation:", "OPERATIONS_LIST": "Operations list", "PATH": "Path:", + "PAY": "Pay", "PREAUTH_TX": "preAuthTx:", "PRICE": "Price:", "PRIVATE_KEY": "Private Key", @@ -129,7 +133,9 @@ "SPANISH": "Spanish", "SPONSORED_ID": "Sponsored ID:", "STARTING_BALANCE": "Starting Balance:", + "SUCCESSFUL_PAYMENT": "The payment was successful!", "TIME_BOUNDS": "Time Bounds:", + "TO_THE_ACCOUNT": "to the account", "TRANSACTION": "Transaction:", "TRUSTOR": "Trustor:", "USER_IS_NOT_CONNECTED": "User is not connected", @@ -137,5 +143,6 @@ "WEIGHT": "Weight:", "XDR_INVALID": "Sorry, the XDR is invalid", "XDR_NOT_PROVIDED": "Sorry, an XDR wasn't provided", + "YOU_ARE_PAYING": "You are paying", "YOUR_ACCOUNT": "Your Account" } diff --git a/src/lib/i18n/languages/spanish.json b/src/lib/i18n/languages/spanish.json index ee97a441..c878f673 100644 --- a/src/lib/i18n/languages/spanish.json +++ b/src/lib/i18n/languages/spanish.json @@ -30,8 +30,10 @@ "ENGLISH_ISO": "en", "ENGLISH": "Inglés", "ERROR": "Error", + "ERROR_MISSING_RECEIVER_DATA": "Lo sentimos, la información del destinatario no fue proporcionada.", "EXPAND_ALL": "Expandir todas", "EXTEND_TO": "Extender a:", + "FAILED_PAYMENT": "Pago fallido. Por favor, inténtelo de nuevo.", "FEE_BUMP_DESCRIPTION_1": "Vas a firmar para pagar la", "FEE_BUMP_DESCRIPTION_2": "de la transacción a continuación", "FEE_BUMP": "FEE BUMP", @@ -40,6 +42,7 @@ "FUNCTION_TYPE": "Tipo de función:", "GO_TO_CONNECT": "Ir a Conectar", "GO_TO_SIGN": "Ir a Firmar", + "GO_TO_PAYMENT": "Ir a Pagar", "HIDE_ALL": "Ocultar todas", "HIDE_KEY": "Ocultar llave", "HIGH_THRESHOLD": "Umbral alto:", @@ -108,6 +111,7 @@ "OPERATION": "Operación:", "OPERATIONS_LIST": "Lista de operaciones", "PATH": "Ruta:", + "PAY": "Pagar", "PREAUTH_TX": "preAuthTx:", "PRICE": "Precio:", "PRIVATE_KEY": "Llave Privada", @@ -129,7 +133,9 @@ "SPANISH": "Español", "SPONSORED_ID": "ID Patrocinada:", "STARTING_BALANCE": "Balance inicial:", + "SUCCESSFUL_PAYMENT": "¡El pago ha sido exitoso!", "TIME_BOUNDS": "Límites de tiempo:", + "TO_THE_ACCOUNT": "a la cuenta", "TRANSACTION": "Transacción:", "TRUSTOR": "Fideicomitente:", "USER_IS_NOT_CONNECTED": "El usuario no está conectado", @@ -137,5 +143,6 @@ "WEIGHT": "Peso:", "XDR_INVALID": "Lo sentimos, el XDR es inválido", "XDR_NOT_PROVIDED": "Lo sentimos, no encontramos ningún XDR", + "YOU_ARE_PAYING": "Estás pagando", "YOUR_ACCOUNT": "Tu Cuenta" } diff --git a/src/lib/stellar/Payment.ts b/src/lib/stellar/Payment.ts new file mode 100644 index 00000000..d0035647 --- /dev/null +++ b/src/lib/stellar/Payment.ts @@ -0,0 +1,33 @@ +import { Asset, BASE_FEE, Operation, TransactionBuilder } from 'stellar-sdk'; + +import { CURRENT_NETWORK_PASSPHRASE } from './StellarNetwork'; +import { server } from './utils'; + +export async function createPaymentTransaction( + publicKey: string, + receiver: string, + amount: string, + assetCode: string, + issuer?: string, +) { + const asset = assetCode === 'native' ? Asset.native() : new Asset(assetCode, issuer); + + try { + const account = await server.loadAccount(publicKey); + return new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: CURRENT_NETWORK_PASSPHRASE, + }) + .addOperation( + Operation.payment({ + destination: receiver, + asset: asset, + amount: amount, + }), + ) + .setTimeout(30) + .build(); + } catch (error) { + throw new Error(JSON.stringify(error)); + } +} diff --git a/src/lib/stellar/utils/index.ts b/src/lib/stellar/utils/index.ts new file mode 100644 index 00000000..2f3c7eb8 --- /dev/null +++ b/src/lib/stellar/utils/index.ts @@ -0,0 +1,5 @@ +import { Horizon } from 'stellar-sdk'; + +import { HORIZON_URL } from '../../../constants'; + +export const server = new Horizon.Server(HORIZON_URL); diff --git a/src/lib/wallets/privateKey/PrivateKey.ts b/src/lib/wallets/privateKey/PrivateKey.ts index f3729e89..5692067a 100644 --- a/src/lib/wallets/privateKey/PrivateKey.ts +++ b/src/lib/wallets/privateKey/PrivateKey.ts @@ -17,13 +17,15 @@ export default class PrivateKey extends AbstractWallet implements IWallet { private PRIVATE_KEY_ITEM_NAME = 'privateKey'; private INITIALIZATION_VECTORS_KEY_ITEM_NAME = 'iv'; - public override async getPublicKey(privateKey: string): Promise { + public override async getPublicKey(privateKey?: string): Promise { let publicKey: string; try { - publicKey = Keypair.fromSecret(privateKey).publicKey(); - super.persistWallet(); + if (privateKey) { + publicKey = Keypair.fromSecret(privateKey).publicKey(); + super.persistWallet(); - await this.storeEncryptedPrivateKey(privateKey); + await this.storeEncryptedPrivateKey(privateKey); + } } catch (e) { if (e instanceof InvalidPrivateKeyError) { console.error('Invalid key, please try again'); diff --git a/src/routes/Home.svelte b/src/routes/Home.svelte index f550e3a7..66ee2d4f 100644 --- a/src/routes/Home.svelte +++ b/src/routes/Home.svelte @@ -12,4 +12,7 @@
{$language.GO_TO_SIGN}
+
+ {$language.GO_TO_PAYMENT} +
diff --git a/src/routes/connect/Connect.svelte b/src/routes/connect/Connect.svelte index f6790869..d78ce89e 100644 --- a/src/routes/connect/Connect.svelte +++ b/src/routes/connect/Connect.svelte @@ -11,6 +11,7 @@ const parent = window.opener; const bridge = new Bridge(SimpleSignerPageType.CONNECT); + const redirect = bridge.getRedirectFromUrl(); $wallets = bridge.getWalletsFromUrl(); if (parent && !$wallets.length) { @@ -26,6 +27,11 @@ const publicKey: string = detail.publicKey; const wallet: IWallet = detail.wallet; bridge.sendOnConnectEvent(publicKey, wallet.getName()); + if (redirect) { + window.location.href = `/${redirect}`; + } else { + bridge.closeWindow(); + } } bridge.sendOnReadyEvent(); diff --git a/src/routes/payment/Payment.svelte b/src/routes/payment/Payment.svelte new file mode 100644 index 00000000..e69487f2 --- /dev/null +++ b/src/routes/payment/Payment.svelte @@ -0,0 +1,309 @@ + + +{#if paymentResultMessage} +
+
{paymentResultMessage}
+ +
+{:else} +
+ {#if !receiver || !amount || !assetCode || !issuer} +

{$language.ERROR}

+
+

{$language.ERROR_MISSING_RECEIVER_DATA}

+ +
+ {:else} +

{$language.PAY}

+
+
+

{$language.NETWORK}:

+

{CURRENT_STELLAR_NETWORK}

+
+
+
+ {$language.YOU_ARE_PAYING} + {amount} + {assetCode === 'native' ? 'XLM' : { assetCode }} + {$language.TO_THE_ACCOUNT} +
+ {receiver}. +
+ + {#if wallet} +
+ + +
+ {:else} +
+

{$language.USER_IS_NOT_CONNECTED}

+ +
+ {/if} + {/if} +
+{/if} + + diff --git a/test.html b/test.html index 1c3244d0..81a1fea6 100644 --- a/test.html +++ b/test.html @@ -16,13 +16,20 @@ + +