@ -0,0 +1,265 @@ |
|||
/* global it, describe, jasmine, afterAll, beforeAll */ |
|||
import { HDSegwitBech32Wallet } from './class'; |
|||
global.crypto = require('crypto'); // shall be used by tests under nodejs CLI, but not in RN environment
|
|||
let assert = require('assert'); |
|||
global.net = require('net'); // needed by Electrum client. For RN it is proviced in shim.js
|
|||
let BlueElectrum = require('./BlueElectrum'); // so it connects ASAP
|
|||
|
|||
afterAll(async () => { |
|||
// after all tests we close socket so the test suite can actually terminate
|
|||
BlueElectrum.forceDisconnect(); |
|||
return new Promise(resolve => setTimeout(resolve, 10000)); // simple sleep to wait for all timeouts termination
|
|||
}); |
|||
|
|||
beforeAll(async () => { |
|||
// awaiting for Electrum to be connected. For RN Electrum would naturally connect
|
|||
// while app starts up, but for tests we need to wait for it
|
|||
await BlueElectrum.waitTillConnected(); |
|||
}); |
|||
|
|||
describe('Bech32 Segwit HD (BIP84)', () => { |
|||
it('can create', async function() { |
|||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 30 * 1000; |
|||
let mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; |
|||
let hd = new HDSegwitBech32Wallet(); |
|||
hd.setSecret(mnemonic); |
|||
|
|||
assert.strictEqual(true, hd.validateMnemonic()); |
|||
assert.strictEqual( |
|||
'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs', |
|||
hd.getXpub(), |
|||
); |
|||
|
|||
assert.strictEqual(hd._getExternalWIFByIndex(0), 'KyZpNDKnfs94vbrwhJneDi77V6jF64PWPF8x5cdJb8ifgg2DUc9d'); |
|||
assert.strictEqual(hd._getExternalWIFByIndex(1), 'Kxpf5b8p3qX56DKEe5NqWbNUP9MnqoRFzZwHRtsFqhzuvUJsYZCy'); |
|||
assert.strictEqual(hd._getInternalWIFByIndex(0), 'KxuoxufJL5csa1Wieb2kp29VNdn92Us8CoaUG3aGtPtcF3AzeXvF'); |
|||
assert.ok(hd._getInternalWIFByIndex(0) !== hd._getInternalWIFByIndex(1)); |
|||
|
|||
assert.strictEqual(hd._getExternalAddressByIndex(0), 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu'); |
|||
assert.strictEqual(hd._getExternalAddressByIndex(1), 'bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g'); |
|||
assert.strictEqual(hd._getInternalAddressByIndex(0), 'bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el'); |
|||
assert.ok(hd._getInternalAddressByIndex(0) !== hd._getInternalAddressByIndex(1)); |
|||
|
|||
assert.ok(hd._lastBalanceFetch === 0); |
|||
await hd.fetchBalance(); |
|||
assert.strictEqual(hd.getBalance(), 0); |
|||
assert.ok(hd._lastBalanceFetch > 0); |
|||
|
|||
// checking that internal pointer and async address getter return the same address
|
|||
let freeAddress = await hd.getAddressAsync(); |
|||
assert.strictEqual(hd.next_free_address_index, 0); |
|||
assert.strictEqual(hd._getExternalAddressByIndex(hd.next_free_address_index), freeAddress); |
|||
let freeChangeAddress = await hd.getChangeAddressAsync(); |
|||
assert.strictEqual(hd.next_free_change_address_index, 0); |
|||
assert.strictEqual(hd._getInternalAddressByIndex(hd.next_free_change_address_index), freeChangeAddress); |
|||
}); |
|||
|
|||
it('can fetch balance', async function() { |
|||
if (!process.env.HD_MNEMONIC) { |
|||
console.error('process.env.HD_MNEMONIC not set, skipped'); |
|||
return; |
|||
} |
|||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 90 * 1000; |
|||
let hd = new HDSegwitBech32Wallet(); |
|||
hd.setSecret(process.env.HD_MNEMONIC); |
|||
assert.ok(hd.validateMnemonic()); |
|||
|
|||
assert.strictEqual( |
|||
'zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP', |
|||
hd.getXpub(), |
|||
); |
|||
|
|||
assert.strictEqual(hd._getExternalAddressByIndex(0), 'bc1qvd6w54sydc08z3802svkxr7297ez7cusd6266p'); |
|||
assert.strictEqual(hd._getExternalAddressByIndex(1), 'bc1qt4t9xl2gmjvxgmp5gev6m8e6s9c85979ta7jeh'); |
|||
assert.strictEqual(hd._getInternalAddressByIndex(0), 'bc1qcg6e26vtzja0h8up5w2m7utex0fsu4v0e0e7uy'); |
|||
assert.strictEqual(hd._getInternalAddressByIndex(1), 'bc1qwp58x4c9e5cplsnw5096qzdkae036ug7a34x3r'); |
|||
|
|||
await hd.fetchBalance(); |
|||
assert.strictEqual(hd.getBalance(), 200000); |
|||
assert.strictEqual(await hd.getAddressAsync(), hd._getExternalAddressByIndex(2)); |
|||
assert.strictEqual(await hd.getChangeAddressAsync(), hd._getInternalAddressByIndex(2)); |
|||
assert.strictEqual(hd.next_free_address_index, 2); |
|||
assert.strictEqual(hd.next_free_change_address_index, 2); |
|||
|
|||
// now, reset HD wallet, and find free addresses from scratch:
|
|||
hd = new HDSegwitBech32Wallet(); |
|||
hd.setSecret(process.env.HD_MNEMONIC); |
|||
|
|||
assert.strictEqual(await hd.getAddressAsync(), hd._getExternalAddressByIndex(2)); |
|||
assert.strictEqual(await hd.getChangeAddressAsync(), hd._getInternalAddressByIndex(2)); |
|||
assert.strictEqual(hd.next_free_address_index, 2); |
|||
assert.strictEqual(hd.next_free_change_address_index, 2); |
|||
}); |
|||
|
|||
it('can fetch transactions', async function() { |
|||
if (!process.env.HD_MNEMONIC) { |
|||
console.error('process.env.HD_MNEMONIC not set, skipped'); |
|||
return; |
|||
} |
|||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 90 * 1000; |
|||
let hd = new HDSegwitBech32Wallet(); |
|||
hd.setSecret(process.env.HD_MNEMONIC); |
|||
assert.ok(hd.validateMnemonic()); |
|||
|
|||
assert.strictEqual(hd.timeToRefreshBalance(), true); |
|||
assert.ok(hd._lastTxFetch === 0); |
|||
assert.ok(hd._lastBalanceFetch === 0); |
|||
await hd.fetchBalance(); |
|||
await hd.fetchTransactions(); |
|||
assert.ok(hd._lastTxFetch > 0); |
|||
assert.ok(hd._lastBalanceFetch > 0); |
|||
assert.strictEqual(hd.timeToRefreshBalance(), false); |
|||
assert.strictEqual(hd.getTransactions().length, 4); |
|||
|
|||
for (let tx of hd.getTransactions()) { |
|||
assert.ok(tx.hash); |
|||
assert.strictEqual(tx.value, 50000); |
|||
assert.ok(tx.received); |
|||
assert.ok(tx.confirmations > 1); |
|||
} |
|||
}); |
|||
|
|||
it('can fetch UTXO', async () => { |
|||
if (!process.env.HD_MNEMONIC) { |
|||
console.error('process.env.HD_MNEMONIC not set, skipped'); |
|||
return; |
|||
} |
|||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 90 * 1000; |
|||
let hd = new HDSegwitBech32Wallet(); |
|||
hd.setSecret(process.env.HD_MNEMONIC); |
|||
assert.ok(hd.validateMnemonic()); |
|||
|
|||
await hd.fetchBalance(); |
|||
await hd.fetchUtxo(); |
|||
let utxo = hd.getUtxo(); |
|||
assert.strictEqual(utxo.length, 4); |
|||
assert.ok(utxo[0].txId); |
|||
assert.ok(utxo[0].vout === 0 || utxo[0].vout === 1); |
|||
assert.ok(utxo[0].value); |
|||
assert.ok(utxo[0].address); |
|||
}); |
|||
|
|||
it('can generate addresses only via zpub', function() { |
|||
let zpub = 'zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs'; |
|||
let hd = new HDSegwitBech32Wallet(); |
|||
hd._xpub = zpub; |
|||
assert.strictEqual(hd._getExternalAddressByIndex(0), 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu'); |
|||
assert.strictEqual(hd._getExternalAddressByIndex(1), 'bc1qnjg0jd8228aq7egyzacy8cys3knf9xvrerkf9g'); |
|||
assert.strictEqual(hd._getInternalAddressByIndex(0), 'bc1q8c6fshw2dlwun7ekn9qwf37cu2rn755upcp6el'); |
|||
assert.ok(hd._getInternalAddressByIndex(0) !== hd._getInternalAddressByIndex(1)); |
|||
}); |
|||
|
|||
it('can generate', async () => { |
|||
let hd = new HDSegwitBech32Wallet(); |
|||
let hashmap = {}; |
|||
for (let c = 0; c < 1000; c++) { |
|||
await hd.generate(); |
|||
let secret = hd.getSecret(); |
|||
if (hashmap[secret]) { |
|||
throw new Error('Duplicate secret generated!'); |
|||
} |
|||
hashmap[secret] = 1; |
|||
assert.ok(secret.split(' ').length === 12 || secret.split(' ').length === 24); |
|||
} |
|||
|
|||
let hd2 = new HDSegwitBech32Wallet(); |
|||
hd2.setSecret(hd.getSecret()); |
|||
assert.ok(hd2.validateMnemonic()); |
|||
}); |
|||
|
|||
it('can catch up with externally modified wallet', async () => { |
|||
if (!process.env.HD_MNEMONIC_BIP84) { |
|||
console.error('process.env.HD_MNEMONIC_BIP84 not set, skipped'); |
|||
return; |
|||
} |
|||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 90 * 1000; |
|||
let hd = new HDSegwitBech32Wallet(); |
|||
hd.setSecret(process.env.HD_MNEMONIC_BIP84); |
|||
assert.ok(hd.validateMnemonic()); |
|||
|
|||
await hd.fetchBalance(); |
|||
let oldBalance = hd.getBalance(); |
|||
|
|||
await hd.fetchTransactions(); |
|||
let oldTransactions = hd.getTransactions(); |
|||
|
|||
// now, mess with internal state, make it 'obsolete'
|
|||
|
|||
hd._txs_by_external_index['2'].pop(); |
|||
hd._txs_by_internal_index['16'].pop(); |
|||
hd._txs_by_internal_index['17'] = []; |
|||
|
|||
for (let c = 17; c < 100; c++) hd._balances_by_internal_index[c] = { c: 0, u: 0 }; |
|||
hd._balances_by_external_index['2'].c = 1000000; |
|||
|
|||
assert.ok(hd.getBalance() !== oldBalance); |
|||
assert.ok(hd.getTransactions().length !== oldTransactions.length); |
|||
|
|||
// now, refetch! should get back to normal
|
|||
|
|||
await hd.fetchBalance(); |
|||
assert.strictEqual(hd.getBalance(), oldBalance); |
|||
await hd.fetchTransactions(); |
|||
assert.strictEqual(hd.getTransactions().length, oldTransactions.length); |
|||
}); |
|||
|
|||
it('can create transactions', async () => { |
|||
if (!process.env.HD_MNEMONIC_BIP84) { |
|||
console.error('process.env.HD_MNEMONIC_BIP84 not set, skipped'); |
|||
return; |
|||
} |
|||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 90 * 1000; |
|||
let hd = new HDSegwitBech32Wallet(); |
|||
hd.setSecret(process.env.HD_MNEMONIC_BIP84); |
|||
assert.ok(hd.validateMnemonic()); |
|||
|
|||
let start = +new Date(); |
|||
await hd.fetchBalance(); |
|||
let end = +new Date(); |
|||
end - start > 5000 && console.warn('fetchBalance took', (end - start) / 1000, 'sec'); |
|||
|
|||
start = +new Date(); |
|||
await hd.fetchTransactions(); |
|||
end = +new Date(); |
|||
end - start > 15000 && console.warn('fetchTransactions took', (end - start) / 1000, 'sec'); |
|||
|
|||
let txFound = 0; |
|||
for (let tx of hd.getTransactions()) { |
|||
if (tx.hash === 'e9ef58baf4cff3ad55913a360c2fa1fd124309c59dcd720cdb172ce46582097b') { |
|||
assert.strictEqual(tx.value, -129545); |
|||
txFound++; |
|||
} |
|||
if (tx.hash === 'e112771fd43962abfe4e4623bf788d6d95ff1bd0f9b56a6a41fb9ed4dacc75f1') { |
|||
assert.strictEqual(tx.value, 1000000); |
|||
txFound++; |
|||
} |
|||
} |
|||
assert.ok(txFound === 2); |
|||
|
|||
await hd.fetchUtxo(); |
|||
let changeAddress = await hd.getChangeAddressAsync(); |
|||
assert.ok(changeAddress && changeAddress.startsWith('bc1')); |
|||
|
|||
let { tx, inputs, outputs, fee } = hd.createTransaction( |
|||
hd.getUtxo(), |
|||
[{ address: 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu', value: 101000 }], |
|||
13, |
|||
changeAddress, |
|||
); |
|||
|
|||
assert.strictEqual(Math.round(fee / tx.byteLength()), 13); |
|||
|
|||
let totalInput = 0; |
|||
for (let inp of inputs) { |
|||
totalInput += inp.value; |
|||
} |
|||
|
|||
let totalOutput = 0; |
|||
for (let outp of outputs) { |
|||
totalOutput += outp.value; |
|||
} |
|||
|
|||
assert.strictEqual(totalInput - totalOutput, fee); |
|||
assert.strictEqual(outputs[outputs.length - 1].address, changeAddress); |
|||
}); |
|||
}); |
@ -0,0 +1,138 @@ |
|||
import * as watch from 'react-native-watch-connectivity'; |
|||
import { InteractionManager } from 'react-native'; |
|||
const loc = require('./loc'); |
|||
export default class WatchConnectivity { |
|||
isAppInstalled = false; |
|||
BlueApp = require('./BlueApp'); |
|||
|
|||
constructor() { |
|||
this.getIsWatchAppInstalled(); |
|||
} |
|||
|
|||
getIsWatchAppInstalled() { |
|||
watch.getIsWatchAppInstalled((err, isAppInstalled) => { |
|||
if (!err) { |
|||
this.isAppInstalled = isAppInstalled; |
|||
this.sendWalletsToWatch(); |
|||
} |
|||
}); |
|||
watch.subscribeToMessages(async (err, message, reply) => { |
|||
if (!err) { |
|||
if (message.request === 'createInvoice') { |
|||
const createInvoiceRequest = await this.handleLightningInvoiceCreateRequest( |
|||
message.walletIndex, |
|||
message.amount, |
|||
message.description, |
|||
); |
|||
reply({ invoicePaymentRequest: createInvoiceRequest }); |
|||
} |
|||
} else { |
|||
reply(err); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
async handleLightningInvoiceCreateRequest(walletIndex, amount, description) { |
|||
const wallet = this.BlueApp.getWallets()[walletIndex]; |
|||
if (wallet.allowReceive() && amount > 0 && description.trim().length > 0) { |
|||
try { |
|||
const invoiceRequest = await wallet.addInvoice(amount, description); |
|||
return invoiceRequest; |
|||
} catch (error) { |
|||
return error; |
|||
} |
|||
} |
|||
} |
|||
|
|||
async sendWalletsToWatch() { |
|||
InteractionManager.runAfterInteractions(async () => { |
|||
if (this.isAppInstalled) { |
|||
const allWallets = this.BlueApp.getWallets(); |
|||
let wallets = []; |
|||
for (const wallet of allWallets) { |
|||
let receiveAddress = ''; |
|||
if (wallet.allowReceive()) { |
|||
if (wallet.getAddressAsync) { |
|||
receiveAddress = await wallet.getAddressAsync(); |
|||
} else { |
|||
receiveAddress = wallet.getAddress(); |
|||
} |
|||
} |
|||
let transactions = wallet.getTransactions(10); |
|||
let watchTransactions = []; |
|||
for (const transaction of transactions) { |
|||
let type = 'pendingConfirmation'; |
|||
let memo = ''; |
|||
let amount = 0; |
|||
|
|||
if (transaction.hasOwnProperty('confirmations') && !transaction.confirmations > 0) { |
|||
type = 'pendingConfirmation'; |
|||
} else if (transaction.type === 'user_invoice' || transaction.type === 'payment_request') { |
|||
const currentDate = new Date(); |
|||
const now = (currentDate.getTime() / 1000) | 0; |
|||
const invoiceExpiration = transaction.timestamp + transaction.expire_time; |
|||
|
|||
if (invoiceExpiration > now) { |
|||
type = 'pendingConfirmation'; |
|||
} else if (invoiceExpiration < now) { |
|||
if (transaction.ispaid) { |
|||
type = 'received'; |
|||
} else { |
|||
type = 'sent'; |
|||
} |
|||
} |
|||
} else if (transaction.value / 100000000 < 0) { |
|||
type = 'sent'; |
|||
} else { |
|||
type = 'received'; |
|||
} |
|||
if (transaction.type === 'user_invoice' || transaction.type === 'payment_request') { |
|||
if (isNaN(transaction.value)) { |
|||
amount = '0'; |
|||
} |
|||
const currentDate = new Date(); |
|||
const now = (currentDate.getTime() / 1000) | 0; |
|||
const invoiceExpiration = transaction.timestamp + transaction.expire_time; |
|||
|
|||
if (invoiceExpiration > now) { |
|||
amount = loc.formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString(); |
|||
} else if (invoiceExpiration < now) { |
|||
if (transaction.ispaid) { |
|||
amount = loc.formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString(); |
|||
} else { |
|||
amount = loc.lnd.expired; |
|||
} |
|||
} else { |
|||
amount = loc.formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString(); |
|||
} |
|||
} else { |
|||
amount = loc.formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString(); |
|||
} |
|||
if (this.BlueApp.tx_metadata[transaction.hash] && this.BlueApp.tx_metadata[transaction.hash]['memo']) { |
|||
memo = this.BlueApp.tx_metadata[transaction.hash]['memo']; |
|||
} else if (transaction.memo) { |
|||
memo = transaction.memo; |
|||
} |
|||
const watchTX = { type, amount, memo, time: loc.transactionTimeToReadable(transaction.received) }; |
|||
watchTransactions.push(watchTX); |
|||
} |
|||
wallets.push({ |
|||
label: wallet.getLabel(), |
|||
balance: loc.formatBalance(Number(wallet.getBalance()), wallet.getPreferredBalanceUnit(), true), |
|||
type: wallet.type, |
|||
preferredBalanceUnit: wallet.getPreferredBalanceUnit(), |
|||
receiveAddress: receiveAddress, |
|||
transactions: watchTransactions, |
|||
}); |
|||
} |
|||
|
|||
watch.updateApplicationContext({ wallets }); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
WatchConnectivity.init = function() { |
|||
if (WatchConnectivity.shared) return; |
|||
WatchConnectivity.shared = new WatchConnectivity(); |
|||
}; |
@ -0,0 +1,114 @@ |
|||
/* global it, describe, jasmine, afterAll, beforeAll */ |
|||
import { WatchOnlyWallet } from './class'; |
|||
let assert = require('assert'); |
|||
global.net = require('net'); // needed by Electrum client. For RN it is proviced in shim.js
|
|||
let BlueElectrum = require('./BlueElectrum'); // so it connects ASAP
|
|||
|
|||
afterAll(async () => { |
|||
// after all tests we close socket so the test suite can actually terminate
|
|||
BlueElectrum.forceDisconnect(); |
|||
return new Promise(resolve => setTimeout(resolve, 10000)); // simple sleep to wait for all timeouts termination
|
|||
}); |
|||
|
|||
beforeAll(async () => { |
|||
// awaiting for Electrum to be connected. For RN Electrum would naturally connect
|
|||
// while app starts up, but for tests we need to wait for it
|
|||
await BlueElectrum.waitTillConnected(); |
|||
}); |
|||
|
|||
describe('Watch only wallet', () => { |
|||
it('can fetch balance', async () => { |
|||
let w = new WatchOnlyWallet(); |
|||
w.setSecret('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa'); |
|||
await w.fetchBalance(); |
|||
assert.ok(w.getBalance() > 16); |
|||
}); |
|||
|
|||
it('can fetch tx', async () => { |
|||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 150 * 1000; |
|||
let w = new WatchOnlyWallet(); |
|||
|
|||
w.setSecret('167zK5iZrs1U6piDqubD3FjRqUTM2CZnb8'); |
|||
await w.fetchTransactions(); |
|||
assert.strictEqual(w.getTransactions().length, 233); |
|||
|
|||
w = new WatchOnlyWallet(); |
|||
w.setSecret('1BiJW1jyUaxcJp2JWwbPLPzB1toPNWTFJV'); |
|||
await w.fetchTransactions(); |
|||
assert.strictEqual(w.getTransactions().length, 2); |
|||
|
|||
// fetch again and make sure no duplicates
|
|||
await w.fetchTransactions(); |
|||
assert.strictEqual(w.getTransactions().length, 2); |
|||
}); |
|||
|
|||
it('can fetch complex TXs', async () => { |
|||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 120 * 1000; |
|||
let w = new WatchOnlyWallet(); |
|||
w.setSecret('3NLnALo49CFEF4tCRhCvz45ySSfz3UktZC'); |
|||
await w.fetchTransactions(); |
|||
for (let tx of w.getTransactions()) { |
|||
assert.ok(tx.value, 'incorrect tx.value'); |
|||
} |
|||
}); |
|||
|
|||
it('can validate address', async () => { |
|||
let w = new WatchOnlyWallet(); |
|||
w.setSecret('12eQ9m4sgAwTSQoNXkRABKhCXCsjm2jdVG'); |
|||
assert.ok(w.valid()); |
|||
w.setSecret('3BDsBDxDimYgNZzsqszNZobqQq3yeUoJf2'); |
|||
assert.ok(w.valid()); |
|||
w.setSecret('not valid'); |
|||
assert.ok(!w.valid()); |
|||
|
|||
w.setSecret('xpub6CQdfC3v9gU86eaSn7AhUFcBVxiGhdtYxdC5Cw2vLmFkfth2KXCMmYcPpvZviA89X6DXDs4PJDk5QVL2G2xaVjv7SM4roWHr1gR4xB3Z7Ps'); |
|||
assert.ok(w.valid()); |
|||
w.setSecret('ypub6XRzrn3HB1tjhhvrHbk1vnXCecZEdXohGzCk3GXwwbDoJ3VBzZ34jNGWbC6WrS7idXrYjjXEzcPDX5VqnHEnuNf5VAXgLfSaytMkJ2rwVqy'); |
|||
assert.ok(w.valid()); |
|||
w.setSecret('zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP'); |
|||
assert.ok(w.valid()); |
|||
}); |
|||
|
|||
it('can fetch balance & transactions from zpub HD', async () => { |
|||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000; |
|||
let w = new WatchOnlyWallet(); |
|||
w.setSecret('zpub6r7jhKKm7BAVx3b3nSnuadY1WnshZYkhK8gKFoRLwK9rF3Mzv28BrGcCGA3ugGtawi1WLb2vyjQAX9ZTDGU5gNk2bLdTc3iEXr6tzR1ipNP'); |
|||
await w.fetchBalance(); |
|||
assert.strictEqual(w.getBalance(), 200000); |
|||
await w.fetchTransactions(); |
|||
assert.strictEqual(w.getTransactions().length, 4); |
|||
assert.ok((await w.getAddressAsync()).startsWith('bc1')); |
|||
}); |
|||
|
|||
it('can fetch balance & transactions from ypub HD', async () => { |
|||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000; |
|||
let w = new WatchOnlyWallet(); |
|||
w.setSecret('ypub6XRzrn3HB1tjhhvrHbk1vnXCecZEdXohGzCk3GXwwbDoJ3VBzZ34jNGWbC6WrS7idXrYjjXEzcPDX5VqnHEnuNf5VAXgLfSaytMkJ2rwVqy'); |
|||
await w.fetchBalance(); |
|||
assert.strictEqual(w.getBalance(), 52774); |
|||
await w.fetchTransactions(); |
|||
assert.strictEqual(w.getTransactions().length, 3); |
|||
assert.ok((await w.getAddressAsync()).startsWith('3')); |
|||
}); |
|||
|
|||
it('can fetch balance & transactions from xpub HD', async () => { |
|||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100 * 1000; |
|||
let w = new WatchOnlyWallet(); |
|||
w.setSecret('xpub6CQdfC3v9gU86eaSn7AhUFcBVxiGhdtYxdC5Cw2vLmFkfth2KXCMmYcPpvZviA89X6DXDs4PJDk5QVL2G2xaVjv7SM4roWHr1gR4xB3Z7Ps'); |
|||
await w.fetchBalance(); |
|||
assert.strictEqual(w.getBalance(), 0); |
|||
await w.fetchTransactions(); |
|||
assert.strictEqual(w.getTransactions().length, 4); |
|||
assert.ok((await w.getAddressAsync()).startsWith('1')); |
|||
}); |
|||
|
|||
it('can fetch large HD', async () => { |
|||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 500 * 1000; |
|||
let w = new WatchOnlyWallet(); |
|||
w.setSecret('ypub6WnnYxkQCGeowv4BXq9Y9PHaXgHMJg9TkFaDJkunhcTAfbDw8z3LvV9kFNHGjeVaEoGdsSJgaMWpUBvYvpYGMJd43gTK5opecVVkvLwKttx'); |
|||
await w.fetchBalance(); |
|||
|
|||
await w.fetchTransactions(); |
|||
assert.ok(w.getTransactions().length >= 167); |
|||
}); |
|||
}); |
@ -0,0 +1 @@ |
|||
export default from '@react-native-community/async-storage/jest/async-storage-mock' |
@ -0,0 +1,492 @@ |
|||
import { AbstractHDWallet } from './abstract-hd-wallet'; |
|||
import { NativeModules } from 'react-native'; |
|||
import bip39 from 'bip39'; |
|||
import BigNumber from 'bignumber.js'; |
|||
import b58 from 'bs58check'; |
|||
const BlueElectrum = require('../BlueElectrum'); |
|||
const bitcoin5 = require('bitcoinjs5'); |
|||
const HDNode = require('bip32'); |
|||
const coinSelectAccumulative = require('coinselect/accumulative'); |
|||
const coinSelectSplit = require('coinselect/split'); |
|||
|
|||
const { RNRandomBytes } = NativeModules; |
|||
|
|||
/** |
|||
* Converts zpub to xpub |
|||
* |
|||
* @param {String} zpub |
|||
* @returns {String} xpub |
|||
*/ |
|||
function _zpubToXpub(zpub) { |
|||
let data = b58.decode(zpub); |
|||
data = data.slice(4); |
|||
data = Buffer.concat([Buffer.from('0488b21e', 'hex'), data]); |
|||
|
|||
return b58.encode(data); |
|||
} |
|||
|
|||
/** |
|||
* Creates Segwit Bech32 Bitcoin address |
|||
* |
|||
* @param hdNode |
|||
* @returns {String} |
|||
*/ |
|||
function _nodeToBech32SegwitAddress(hdNode) { |
|||
return bitcoin5.payments.p2wpkh({ |
|||
pubkey: hdNode.publicKey, |
|||
}).address; |
|||
} |
|||
|
|||
/** |
|||
* HD Wallet (BIP39). |
|||
* In particular, BIP84 (Bech32 Native Segwit) |
|||
* @see https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki
|
|||
*/ |
|||
export class HDSegwitBech32Wallet extends AbstractHDWallet { |
|||
static type = 'HDsegwitBech32'; |
|||
static typeReadable = 'HD SegWit (BIP84 Bech32 Native)'; |
|||
|
|||
constructor() { |
|||
super(); |
|||
this._balances_by_external_index = {}; // 0 => { c: 0, u: 0 } // confirmed/unconfirmed
|
|||
this._balances_by_internal_index = {}; |
|||
|
|||
this._txs_by_external_index = {}; |
|||
this._txs_by_internal_index = {}; |
|||
|
|||
this._utxo = []; |
|||
} |
|||
|
|||
/** |
|||
* @inheritDoc |
|||
*/ |
|||
getBalance() { |
|||
let ret = 0; |
|||
for (let bal of Object.values(this._balances_by_external_index)) { |
|||
ret += bal.c; |
|||
} |
|||
for (let bal of Object.values(this._balances_by_internal_index)) { |
|||
ret += bal.c; |
|||
} |
|||
return ret + this.getUnconfirmedBalance(); |
|||
} |
|||
|
|||
/** |
|||
* @inheritDoc |
|||
*/ |
|||
timeToRefreshTransaction() { |
|||
for (let tx of this.getTransactions()) { |
|||
if (tx.confirmations < 7) return true; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
getUnconfirmedBalance() { |
|||
let ret = 0; |
|||
for (let bal of Object.values(this._balances_by_external_index)) { |
|||
ret += bal.u; |
|||
} |
|||
for (let bal of Object.values(this._balances_by_internal_index)) { |
|||
ret += bal.u; |
|||
} |
|||
return ret; |
|||
} |
|||
|
|||
allowSend() { |
|||
return true; |
|||
} |
|||
|
|||
async generate() { |
|||
let that = this; |
|||
return new Promise(function(resolve) { |
|||
if (typeof RNRandomBytes === 'undefined') { |
|||
// CLI/CI environment
|
|||
// crypto should be provided globally by test launcher
|
|||
return crypto.randomBytes(32, (err, buf) => { // eslint-disable-line
|
|||
if (err) throw err; |
|||
that.secret = bip39.entropyToMnemonic(buf.toString('hex')); |
|||
resolve(); |
|||
}); |
|||
} |
|||
|
|||
// RN environment
|
|||
RNRandomBytes.randomBytes(32, (err, bytes) => { |
|||
if (err) throw new Error(err); |
|||
let b = Buffer.from(bytes, 'base64').toString('hex'); |
|||
that.secret = bip39.entropyToMnemonic(b); |
|||
resolve(); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
_getExternalWIFByIndex(index) { |
|||
return this._getWIFByIndex(false, index); |
|||
} |
|||
|
|||
_getInternalWIFByIndex(index) { |
|||
return this._getWIFByIndex(true, index); |
|||
} |
|||
|
|||
/** |
|||
* Get internal/external WIF by wallet index |
|||
* @param {Boolean} internal |
|||
* @param {Number} index |
|||
* @returns {*} |
|||
* @private |
|||
*/ |
|||
_getWIFByIndex(internal, index) { |
|||
const mnemonic = this.secret; |
|||
const seed = bip39.mnemonicToSeed(mnemonic); |
|||
const root = HDNode.fromSeed(seed); |
|||
const path = `m/84'/0'/0'/${internal ? 1 : 0}/${index}`; |
|||
const child = root.derivePath(path); |
|||
|
|||
return child.toWIF(); |
|||
} |
|||
|
|||
_getNodeAddressByIndex(node, index) { |
|||
index = index * 1; // cast to int
|
|||
if (node === 0) { |
|||
if (this.external_addresses_cache[index]) return this.external_addresses_cache[index]; // cache hit
|
|||
} |
|||
|
|||
if (node === 1) { |
|||
if (this.internal_addresses_cache[index]) return this.internal_addresses_cache[index]; // cache hit
|
|||
} |
|||
|
|||
if (node === 0 && !this._node0) { |
|||
const xpub = _zpubToXpub(this.getXpub()); |
|||
const hdNode = HDNode.fromBase58(xpub); |
|||
this._node0 = hdNode.derive(node); |
|||
} |
|||
|
|||
if (node === 1 && !this._node1) { |
|||
const xpub = _zpubToXpub(this.getXpub()); |
|||
const hdNode = HDNode.fromBase58(xpub); |
|||
this._node1 = hdNode.derive(node); |
|||
} |
|||
|
|||
let address; |
|||
if (node === 0) { |
|||
address = _nodeToBech32SegwitAddress(this._node0.derive(index)); |
|||
} |
|||
|
|||
if (node === 1) { |
|||
address = _nodeToBech32SegwitAddress(this._node1.derive(index)); |
|||
} |
|||
|
|||
if (node === 0) { |
|||
return (this.external_addresses_cache[index] = address); |
|||
} |
|||
|
|||
if (node === 1) { |
|||
return (this.internal_addresses_cache[index] = address); |
|||
} |
|||
} |
|||
|
|||
_getExternalAddressByIndex(index) { |
|||
return this._getNodeAddressByIndex(0, index); |
|||
} |
|||
|
|||
_getInternalAddressByIndex(index) { |
|||
return this._getNodeAddressByIndex(1, index); |
|||
} |
|||
|
|||
/** |
|||
* Returning zpub actually, not xpub. Keeping same method name |
|||
* for compatibility. |
|||
* |
|||
* @return {String} zpub |
|||
*/ |
|||
getXpub() { |
|||
if (this._xpub) { |
|||
return this._xpub; // cache hit
|
|||
} |
|||
// first, getting xpub
|
|||
const mnemonic = this.secret; |
|||
const seed = bip39.mnemonicToSeed(mnemonic); |
|||
const root = HDNode.fromSeed(seed); |
|||
|
|||
const path = "m/84'/0'/0'"; |
|||
const child = root.derivePath(path).neutered(); |
|||
const xpub = child.toBase58(); |
|||
|
|||
// bitcoinjs does not support zpub yet, so we just convert it from xpub
|
|||
let data = b58.decode(xpub); |
|||
data = data.slice(4); |
|||
data = Buffer.concat([Buffer.from('04b24746', 'hex'), data]); |
|||
this._xpub = b58.encode(data); |
|||
|
|||
return this._xpub; |
|||
} |
|||
|
|||
/** |
|||
* @inheritDoc |
|||
*/ |
|||
async fetchTransactions() { |
|||
// if txs are absent for some internal address in hierarchy - this is a sign
|
|||
// we should fetch txs for that address
|
|||
// OR if some address has unconfirmed balance - should fetch it's txs
|
|||
// OR some tx for address is unconfirmed
|
|||
|
|||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { |
|||
// external addresses first
|
|||
let hasUnconfirmed = false; |
|||
this._txs_by_external_index[c] = this._txs_by_external_index[c] || []; |
|||
for (let tx of this._txs_by_external_index[c]) hasUnconfirmed = hasUnconfirmed || (!tx.confirmations || tx.confirmations === 0); |
|||
|
|||
if (hasUnconfirmed || this._txs_by_external_index[c].length === 0 || this._balances_by_external_index[c].u !== 0) { |
|||
this._txs_by_external_index[c] = await BlueElectrum.getTransactionsFullByAddress(this._getExternalAddressByIndex(c)); |
|||
} |
|||
} |
|||
|
|||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { |
|||
// next, internal addresses
|
|||
let hasUnconfirmed = false; |
|||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || []; |
|||
for (let tx of this._txs_by_internal_index[c]) hasUnconfirmed = hasUnconfirmed || (!tx.confirmations || tx.confirmations === 0); |
|||
|
|||
if (hasUnconfirmed || this._txs_by_internal_index[c].length === 0 || this._balances_by_internal_index[c].u !== 0) { |
|||
this._txs_by_internal_index[c] = await BlueElectrum.getTransactionsFullByAddress(this._getInternalAddressByIndex(c)); |
|||
} |
|||
} |
|||
|
|||
this._lastTxFetch = +new Date(); |
|||
} |
|||
|
|||
getTransactions() { |
|||
let txs = []; |
|||
|
|||
for (let addressTxs of Object.values(this._txs_by_external_index)) { |
|||
txs = txs.concat(addressTxs); |
|||
} |
|||
for (let addressTxs of Object.values(this._txs_by_internal_index)) { |
|||
txs = txs.concat(addressTxs); |
|||
} |
|||
|
|||
let ret = []; |
|||
for (let tx of txs) { |
|||
tx.received = tx.blocktime * 1000; |
|||
if (!tx.blocktime) tx.received = +new Date() - 30 * 1000; // unconfirmed
|
|||
tx.confirmations = tx.confirmations || 0; // unconfirmed
|
|||
tx.hash = tx.txid; |
|||
tx.value = 0; |
|||
|
|||
for (let vin of tx.inputs) { |
|||
// if input (spending) goes from our address - we are loosing!
|
|||
if (vin.address && this.weOwnAddress(vin.address)) { |
|||
tx.value -= new BigNumber(vin.value).multipliedBy(100000000).toNumber(); |
|||
} |
|||
} |
|||
|
|||
for (let vout of tx.outputs) { |
|||
// when output goes to our address - this means we are gaining!
|
|||
if (vout.addresses && vout.addresses[0] && this.weOwnAddress(vout.scriptPubKey.addresses[0])) { |
|||
tx.value += new BigNumber(vout.value).multipliedBy(100000000).toNumber(); |
|||
} |
|||
} |
|||
ret.push(tx); |
|||
} |
|||
|
|||
// now, deduplication:
|
|||
let usedTxIds = {}; |
|||
let ret2 = []; |
|||
for (let tx of ret) { |
|||
if (!usedTxIds[tx.txid]) ret2.push(tx); |
|||
usedTxIds[tx.txid] = 1; |
|||
} |
|||
|
|||
return ret2.sort(function(a, b) { |
|||
return b.received - a.received; |
|||
}); |
|||
} |
|||
|
|||
async _fetchBalance() { |
|||
// probing future addressess in hierarchy whether they have any transactions, in case
|
|||
// our 'next free addr' pointers are lagging behind
|
|||
let tryAgain = false; |
|||
let txs = await BlueElectrum.getTransactionsByAddress( |
|||
this._getExternalAddressByIndex(this.next_free_address_index + this.gap_limit - 1), |
|||
); |
|||
if (txs.length > 0) { |
|||
// whoa, someone uses our wallet outside! better catch up
|
|||
this.next_free_address_index += this.gap_limit; |
|||
tryAgain = true; |
|||
} |
|||
|
|||
txs = await BlueElectrum.getTransactionsByAddress( |
|||
this._getInternalAddressByIndex(this.next_free_change_address_index + this.gap_limit - 1), |
|||
); |
|||
if (txs.length > 0) { |
|||
this.next_free_change_address_index += this.gap_limit; |
|||
tryAgain = true; |
|||
} |
|||
|
|||
// FIXME: refactor me ^^^ can be batched in single call. plus not just couple of addresses, but all between [ next_free .. (next_free + gap_limit) ]
|
|||
|
|||
if (tryAgain) return this._fetchBalance(); |
|||
|
|||
// next, business as usuall. fetch balances
|
|||
|
|||
let addresses2fetch = []; |
|||
|
|||
// generating all involved addresses.
|
|||
// basically, refetch all from index zero to maximum. doesnt matter
|
|||
// since we batch them 100 per call
|
|||
|
|||
// external
|
|||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { |
|||
addresses2fetch.push(this._getExternalAddressByIndex(c)); |
|||
} |
|||
|
|||
// internal
|
|||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { |
|||
addresses2fetch.push(this._getInternalAddressByIndex(c)); |
|||
} |
|||
|
|||
let balances = await BlueElectrum.multiGetBalanceByAddress(addresses2fetch); |
|||
|
|||
// converting to a more compact internal format
|
|||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { |
|||
let addr = this._getExternalAddressByIndex(c); |
|||
if (balances.addresses[addr]) { |
|||
// first, if balances differ from what we store - we delete transactions for that
|
|||
// address so next fetchTransactions() will refetch everything
|
|||
if (this._balances_by_external_index[c]) { |
|||
if ( |
|||
this._balances_by_external_index[c].c !== balances.addresses[addr].confirmed || |
|||
this._balances_by_external_index[c].u !== balances.addresses[addr].unconfirmed |
|||
) { |
|||
delete this._txs_by_external_index[c]; |
|||
} |
|||
} |
|||
// update local representation of balances on that address:
|
|||
this._balances_by_external_index[c] = { |
|||
c: balances.addresses[addr].confirmed, |
|||
u: balances.addresses[addr].unconfirmed, |
|||
}; |
|||
} |
|||
} |
|||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { |
|||
let addr = this._getInternalAddressByIndex(c); |
|||
if (balances.addresses[addr]) { |
|||
// first, if balances differ from what we store - we delete transactions for that
|
|||
// address so next fetchTransactions() will refetch everything
|
|||
if (this._balances_by_internal_index[c]) { |
|||
if ( |
|||
this._balances_by_internal_index[c].c !== balances.addresses[addr].confirmed || |
|||
this._balances_by_internal_index[c].u !== balances.addresses[addr].unconfirmed |
|||
) { |
|||
delete this._txs_by_internal_index[c]; |
|||
} |
|||
} |
|||
// update local representation of balances on that address:
|
|||
this._balances_by_internal_index[c] = { |
|||
c: balances.addresses[addr].confirmed, |
|||
u: balances.addresses[addr].unconfirmed, |
|||
}; |
|||
} |
|||
} |
|||
|
|||
this._lastBalanceFetch = +new Date(); |
|||
} |
|||
|
|||
async fetchUtxo() { |
|||
// considering only confirmed balance
|
|||
let addressess = []; |
|||
|
|||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { |
|||
if (this._balances_by_external_index[c] && this._balances_by_external_index[c].c && this._balances_by_external_index[c].c > 0) { |
|||
addressess.push(this._getExternalAddressByIndex(c)); |
|||
} |
|||
} |
|||
|
|||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { |
|||
if (this._balances_by_internal_index[c] && this._balances_by_internal_index[c].c && this._balances_by_internal_index[c].c > 0) { |
|||
addressess.push(this._getInternalAddressByIndex(c)); |
|||
} |
|||
} |
|||
|
|||
this._utxo = []; |
|||
for (let arr of Object.values(await BlueElectrum.multiGetUtxoByAddress(addressess))) { |
|||
this._utxo = this._utxo.concat(arr); |
|||
} |
|||
} |
|||
|
|||
getUtxo() { |
|||
return this._utxo; |
|||
} |
|||
|
|||
weOwnAddress(address) { |
|||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) { |
|||
if (this._getExternalAddressByIndex(c) === address) return true; |
|||
} |
|||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) { |
|||
if (this._getInternalAddressByIndex(c) === address) return true; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
createTx(utxos, amount, fee, address) { |
|||
throw new Error('Deprecated'); |
|||
} |
|||
|
|||
/** |
|||
* |
|||
* @param utxos {Array.<{vout: Number, value: Number, txId: String, address: String}>} List of spendable utxos |
|||
* @param targets {Array.<{value: Number, address: String}>} Where coins are going. If theres only 1 target and that target has no value - this will send MAX to that address (respecting fee rate) |
|||
* @param feeRate {Number} satoshi per byte |
|||
* @param changeAddress {String} Excessive coins will go back to that address |
|||
* @param sequence {Number} Used in RBF |
|||
* @returns {{outputs: Array, tx: Transaction, inputs: Array, fee: Number}} |
|||
*/ |
|||
createTransaction(utxos, targets, feeRate, changeAddress, sequence) { |
|||
if (!changeAddress) throw new Error('No change address provided'); |
|||
sequence = sequence || 0; |
|||
|
|||
let algo = coinSelectAccumulative; |
|||
if (targets.length === 1 && targets[0] && !targets[0].value) { |
|||
// we want to send MAX
|
|||
algo = coinSelectSplit; |
|||
} |
|||
|
|||
let { inputs, outputs, fee } = algo(utxos, targets, feeRate); |
|||
|
|||
// .inputs and .outputs will be undefined if no solution was found
|
|||
if (!inputs || !outputs) { |
|||
throw new Error('Not enough balance. Try sending smaller amount'); |
|||
} |
|||
|
|||
let txb = new bitcoin5.TransactionBuilder(); |
|||
|
|||
let c = 0; |
|||
let keypairs = {}; |
|||
let values = {}; |
|||
|
|||
inputs.forEach(input => { |
|||
const keyPair = bitcoin5.ECPair.fromWIF(this._getWifForAddress(input.address)); |
|||
keypairs[c] = keyPair; |
|||
values[c] = input.value; |
|||
c++; |
|||
if (!input.address || !this._getWifForAddress(input.address)) throw new Error('Internal error: no address or WIF to sign input'); |
|||
const p2wpkh = bitcoin5.payments.p2wpkh({ pubkey: keyPair.publicKey }); |
|||
txb.addInput(input.txId, input.vout, sequence, p2wpkh.output); // NOTE: provide the prevOutScript!
|
|||
}); |
|||
|
|||
outputs.forEach(output => { |
|||
// if output has no address - this is change output
|
|||
if (!output.address) { |
|||
output.address = changeAddress; |
|||
} |
|||
|
|||
txb.addOutput(output.address, output.value); |
|||
}); |
|||
|
|||
for (let cc = 0; cc < c; cc++) { |
|||
txb.sign(cc, keypairs[cc], null, null, values[cc]); // NOTE: no redeem script
|
|||
} |
|||
|
|||
const tx = txb.build(); |
|||
return { tx, inputs, outputs, fee }; |
|||
} |
|||
} |
@ -0,0 +1,4 @@ |
|||
vim ios/BlueWallet/Info.plist |
|||
vim ios/BlueWalletWatch/Info.plist |
|||
vim "ios/BlueWalletWatch Extension/Info.plist" |
|||
vim android/app/build.gradle |
@ -0,0 +1,131 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<Scheme |
|||
LastUpgradeVersion = "1020" |
|||
version = "2.0"> |
|||
<BuildAction |
|||
parallelizeBuildables = "YES" |
|||
buildImplicitDependencies = "YES"> |
|||
<BuildActionEntries> |
|||
<BuildActionEntry |
|||
buildForTesting = "YES" |
|||
buildForRunning = "YES" |
|||
buildForProfiling = "YES" |
|||
buildForArchiving = "YES" |
|||
buildForAnalyzing = "YES"> |
|||
<BuildableReference |
|||
BuildableIdentifier = "primary" |
|||
BlueprintIdentifier = "B40D4E2F225841EC00428FCC" |
|||
BuildableName = "BlueWalletWatch.app" |
|||
BlueprintName = "BlueWalletWatch" |
|||
ReferencedContainer = "container:BlueWallet.xcodeproj"> |
|||
</BuildableReference> |
|||
</BuildActionEntry> |
|||
<BuildActionEntry |
|||
buildForTesting = "YES" |
|||
buildForRunning = "YES" |
|||
buildForProfiling = "YES" |
|||
buildForArchiving = "YES" |
|||
buildForAnalyzing = "YES"> |
|||
<BuildableReference |
|||
BuildableIdentifier = "primary" |
|||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A" |
|||
BuildableName = "BlueWallet.app" |
|||
BlueprintName = "BlueWallet" |
|||
ReferencedContainer = "container:BlueWallet.xcodeproj"> |
|||
</BuildableReference> |
|||
</BuildActionEntry> |
|||
</BuildActionEntries> |
|||
</BuildAction> |
|||
<TestAction |
|||
buildConfiguration = "Debug" |
|||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" |
|||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" |
|||
shouldUseLaunchSchemeArgsEnv = "YES"> |
|||
<Testables> |
|||
</Testables> |
|||
<MacroExpansion> |
|||
<BuildableReference |
|||
BuildableIdentifier = "primary" |
|||
BlueprintIdentifier = "B40D4E2F225841EC00428FCC" |
|||
BuildableName = "BlueWalletWatch.app" |
|||
BlueprintName = "BlueWalletWatch" |
|||
ReferencedContainer = "container:BlueWallet.xcodeproj"> |
|||
</BuildableReference> |
|||
</MacroExpansion> |
|||
<AdditionalOptions> |
|||
</AdditionalOptions> |
|||
</TestAction> |
|||
<LaunchAction |
|||
buildConfiguration = "Debug" |
|||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" |
|||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" |
|||
launchStyle = "0" |
|||
useCustomWorkingDirectory = "NO" |
|||
ignoresPersistentStateOnLaunch = "NO" |
|||
debugDocumentVersioning = "YES" |
|||
debugServiceExtension = "internal" |
|||
allowLocationSimulation = "YES" |
|||
launchAutomaticallySubstyle = "8" |
|||
notificationPayloadFile = "BlueWalletWatch Extension/PushNotificationPayload.apns"> |
|||
<RemoteRunnable |
|||
runnableDebuggingMode = "2" |
|||
BundleIdentifier = "com.apple.Carousel" |
|||
RemotePath = "/BlueWallet"> |
|||
<BuildableReference |
|||
BuildableIdentifier = "primary" |
|||
BlueprintIdentifier = "B40D4E2F225841EC00428FCC" |
|||
BuildableName = "BlueWalletWatch.app" |
|||
BlueprintName = "BlueWalletWatch" |
|||
ReferencedContainer = "container:BlueWallet.xcodeproj"> |
|||
</BuildableReference> |
|||
</RemoteRunnable> |
|||
<MacroExpansion> |
|||
<BuildableReference |
|||
BuildableIdentifier = "primary" |
|||
BlueprintIdentifier = "B40D4E2F225841EC00428FCC" |
|||
BuildableName = "BlueWalletWatch.app" |
|||
BlueprintName = "BlueWalletWatch" |
|||
ReferencedContainer = "container:BlueWallet.xcodeproj"> |
|||
</BuildableReference> |
|||
</MacroExpansion> |
|||
<AdditionalOptions> |
|||
</AdditionalOptions> |
|||
</LaunchAction> |
|||
<ProfileAction |
|||
buildConfiguration = "Release" |
|||
shouldUseLaunchSchemeArgsEnv = "YES" |
|||
savedToolIdentifier = "" |
|||
useCustomWorkingDirectory = "NO" |
|||
debugDocumentVersioning = "YES" |
|||
launchAutomaticallySubstyle = "8" |
|||
notificationPayloadFile = "BlueWalletWatch Extension/PushNotificationPayload.apns"> |
|||
<RemoteRunnable |
|||
runnableDebuggingMode = "2" |
|||
BundleIdentifier = "com.apple.Carousel" |
|||
RemotePath = "/BlueWallet"> |
|||
<BuildableReference |
|||
BuildableIdentifier = "primary" |
|||
BlueprintIdentifier = "B40D4E2F225841EC00428FCC" |
|||
BuildableName = "BlueWalletWatch.app" |
|||
BlueprintName = "BlueWalletWatch" |
|||
ReferencedContainer = "container:BlueWallet.xcodeproj"> |
|||
</BuildableReference> |
|||
</RemoteRunnable> |
|||
<MacroExpansion> |
|||
<BuildableReference |
|||
BuildableIdentifier = "primary" |
|||
BlueprintIdentifier = "B40D4E2F225841EC00428FCC" |
|||
BuildableName = "BlueWalletWatch.app" |
|||
BlueprintName = "BlueWalletWatch" |
|||
ReferencedContainer = "container:BlueWallet.xcodeproj"> |
|||
</BuildableReference> |
|||
</MacroExpansion> |
|||
</ProfileAction> |
|||
<AnalyzeAction |
|||
buildConfiguration = "Debug"> |
|||
</AnalyzeAction> |
|||
<ArchiveAction |
|||
buildConfiguration = "Release" |
|||
revealArchiveInOrganizer = "YES"> |
|||
</ArchiveAction> |
|||
</Scheme> |
@ -0,0 +1,128 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<Scheme |
|||
LastUpgradeVersion = "1020" |
|||
version = "1.3"> |
|||
<BuildAction |
|||
parallelizeBuildables = "YES" |
|||
buildImplicitDependencies = "YES"> |
|||
<BuildActionEntries> |
|||
<BuildActionEntry |
|||
buildForTesting = "YES" |
|||
buildForRunning = "YES" |
|||
buildForProfiling = "YES" |
|||
buildForArchiving = "YES" |
|||
buildForAnalyzing = "YES"> |
|||
<BuildableReference |
|||
BuildableIdentifier = "primary" |
|||
BlueprintIdentifier = "B40D4E2F225841EC00428FCC" |
|||
BuildableName = "BlueWalletWatch.app" |
|||
BlueprintName = "BlueWalletWatch" |
|||
ReferencedContainer = "container:BlueWallet.xcodeproj"> |
|||
</BuildableReference> |
|||
</BuildActionEntry> |
|||
<BuildActionEntry |
|||
buildForTesting = "YES" |
|||
buildForRunning = "YES" |
|||
buildForProfiling = "YES" |
|||
buildForArchiving = "YES" |
|||
buildForAnalyzing = "YES"> |
|||
<BuildableReference |
|||
BuildableIdentifier = "primary" |
|||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A" |
|||
BuildableName = "BlueWallet.app" |
|||
BlueprintName = "BlueWallet" |
|||
ReferencedContainer = "container:BlueWallet.xcodeproj"> |
|||
</BuildableReference> |
|||
</BuildActionEntry> |
|||
</BuildActionEntries> |
|||
</BuildAction> |
|||
<TestAction |
|||
buildConfiguration = "Debug" |
|||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" |
|||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" |
|||
shouldUseLaunchSchemeArgsEnv = "YES"> |
|||
<Testables> |
|||
</Testables> |
|||
<MacroExpansion> |
|||
<BuildableReference |
|||
BuildableIdentifier = "primary" |
|||
BlueprintIdentifier = "B40D4E2F225841EC00428FCC" |
|||
BuildableName = "BlueWalletWatch.app" |
|||
BlueprintName = "BlueWalletWatch" |
|||
ReferencedContainer = "container:BlueWallet.xcodeproj"> |
|||
</BuildableReference> |
|||
</MacroExpansion> |
|||
<AdditionalOptions> |
|||
</AdditionalOptions> |
|||
</TestAction> |
|||
<LaunchAction |
|||
buildConfiguration = "Debug" |
|||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" |
|||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" |
|||
launchStyle = "0" |
|||
useCustomWorkingDirectory = "NO" |
|||
ignoresPersistentStateOnLaunch = "NO" |
|||
debugDocumentVersioning = "YES" |
|||
debugServiceExtension = "internal" |
|||
allowLocationSimulation = "YES" |
|||
notificationPayloadFile = "BlueWalletWatch Extension/PushNotificationPayload.apns"> |
|||
<RemoteRunnable |
|||
runnableDebuggingMode = "2" |
|||
BundleIdentifier = "com.apple.Carousel" |
|||
RemotePath = "/BlueWallet"> |
|||
<BuildableReference |
|||
BuildableIdentifier = "primary" |
|||
BlueprintIdentifier = "B40D4E2F225841EC00428FCC" |
|||
BuildableName = "BlueWalletWatch.app" |
|||
BlueprintName = "BlueWalletWatch" |
|||
ReferencedContainer = "container:BlueWallet.xcodeproj"> |
|||
</BuildableReference> |
|||
</RemoteRunnable> |
|||
<MacroExpansion> |
|||
<BuildableReference |
|||
BuildableIdentifier = "primary" |
|||
BlueprintIdentifier = "B40D4E2F225841EC00428FCC" |
|||
BuildableName = "BlueWalletWatch.app" |
|||
BlueprintName = "BlueWalletWatch" |
|||
ReferencedContainer = "container:BlueWallet.xcodeproj"> |
|||
</BuildableReference> |
|||
</MacroExpansion> |
|||
<AdditionalOptions> |
|||
</AdditionalOptions> |
|||
</LaunchAction> |
|||
<ProfileAction |
|||
buildConfiguration = "Release" |
|||
shouldUseLaunchSchemeArgsEnv = "YES" |
|||
savedToolIdentifier = "" |
|||
useCustomWorkingDirectory = "NO" |
|||
debugDocumentVersioning = "YES"> |
|||
<RemoteRunnable |
|||
runnableDebuggingMode = "2" |
|||
BundleIdentifier = "com.apple.Carousel" |
|||
RemotePath = "/BlueWallet"> |
|||
<BuildableReference |
|||
BuildableIdentifier = "primary" |
|||
BlueprintIdentifier = "B40D4E2F225841EC00428FCC" |
|||
BuildableName = "BlueWalletWatch.app" |
|||
BlueprintName = "BlueWalletWatch" |
|||
ReferencedContainer = "container:BlueWallet.xcodeproj"> |
|||
</BuildableReference> |
|||
</RemoteRunnable> |
|||
<MacroExpansion> |
|||
<BuildableReference |
|||
BuildableIdentifier = "primary" |
|||
BlueprintIdentifier = "B40D4E2F225841EC00428FCC" |
|||
BuildableName = "BlueWalletWatch.app" |
|||
BlueprintName = "BlueWalletWatch" |
|||
ReferencedContainer = "container:BlueWallet.xcodeproj"> |
|||
</BuildableReference> |
|||
</MacroExpansion> |
|||
</ProfileAction> |
|||
<AnalyzeAction |
|||
buildConfiguration = "Debug"> |
|||
</AnalyzeAction> |
|||
<ArchiveAction |
|||
buildConfiguration = "Release" |
|||
revealArchiveInOrganizer = "YES"> |
|||
</ArchiveAction> |
|||
</Scheme> |
@ -0,0 +1,19 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<Workspace |
|||
version = "1.0"> |
|||
<FileRef |
|||
location = "group:BlueWallet.xcodeproj"> |
|||
</FileRef> |
|||
<FileRef |
|||
location = "group:Pods/Pods.xcodeproj"> |
|||
</FileRef> |
|||
<FileRef |
|||
location = "group:../node_modules/react-native-tcp/ios/TcpSockets.xcodeproj"> |
|||
</FileRef> |
|||
<FileRef |
|||
location = "group:../node_modules/@remobile/react-native-qrcode-local-image/ios/RCTQRCodeLocalImage.xcodeproj"> |
|||
</FileRef> |
|||
<FileRef |
|||
location = "group:../node_modules/react-native-privacy-snapshot/RCTPrivacySnapshot.xcodeproj"> |
|||
</FileRef> |
|||
</Workspace> |
@ -0,0 +1,8 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
|||
<plist version="1.0"> |
|||
<dict> |
|||
<key>IDEDidComputeMac32BitWarning</key> |
|||
<true/> |
|||
</dict> |
|||
</plist> |
@ -0,0 +1,56 @@ |
|||
// |
|||
// ExtensionDelegate.swift |
|||
// BlueWalletWatch Extension |
|||
// |
|||
// Created by Marcos Rodriguez on 3/6/19. |
|||
// Copyright © 2019 Facebook. All rights reserved. |
|||
// |
|||
|
|||
import WatchKit |
|||
|
|||
class ExtensionDelegate: NSObject, WKExtensionDelegate { |
|||
|
|||
func applicationDidFinishLaunching() { |
|||
// Perform any final initialization of your application. |
|||
} |
|||
|
|||
func applicationDidBecomeActive() { |
|||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. |
|||
} |
|||
|
|||
func applicationWillResignActive() { |
|||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. |
|||
// Use this method to pause ongoing tasks, disable timers, etc. |
|||
} |
|||
|
|||
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) { |
|||
// Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one. |
|||
for task in backgroundTasks { |
|||
// Use a switch statement to check the task type |
|||
switch task { |
|||
case let backgroundTask as WKApplicationRefreshBackgroundTask: |
|||
// Be sure to complete the background task once you’re done. |
|||
backgroundTask.setTaskCompletedWithSnapshot(false) |
|||
case let snapshotTask as WKSnapshotRefreshBackgroundTask: |
|||
// Snapshot tasks have a unique completion call, make sure to set your expiration date |
|||
snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil) |
|||
case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask: |
|||
// Be sure to complete the connectivity task once you’re done. |
|||
connectivityTask.setTaskCompletedWithSnapshot(false) |
|||
case let urlSessionTask as WKURLSessionRefreshBackgroundTask: |
|||
// Be sure to complete the URL session task once you’re done. |
|||
urlSessionTask.setTaskCompletedWithSnapshot(false) |
|||
case let relevantShortcutTask as WKRelevantShortcutRefreshBackgroundTask: |
|||
// Be sure to complete the relevant-shortcut task once you're done. |
|||
relevantShortcutTask.setTaskCompletedWithSnapshot(false) |
|||
case let intentDidRunTask as WKIntentDidRunRefreshBackgroundTask: |
|||
// Be sure to complete the intent-did-run task once you're done. |
|||
intentDidRunTask.setTaskCompletedWithSnapshot(false) |
|||
default: |
|||
// make sure to complete unhandled task types |
|||
task.setTaskCompletedWithSnapshot(false) |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,38 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
|||
<plist version="1.0"> |
|||
<dict> |
|||
<key>CFBundleDevelopmentRegion</key> |
|||
<string>$(DEVELOPMENT_LANGUAGE)</string> |
|||
<key>CFBundleDisplayName</key> |
|||
<string>BlueWalletWatch Extension</string> |
|||
<key>CFBundleExecutable</key> |
|||
<string>$(EXECUTABLE_NAME)</string> |
|||
<key>CFBundleIdentifier</key> |
|||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> |
|||
<key>CFBundleInfoDictionaryVersion</key> |
|||
<string>6.0</string> |
|||
<key>CFBundleName</key> |
|||
<string>$(PRODUCT_NAME)</string> |
|||
<key>CFBundlePackageType</key> |
|||
<string>XPC!</string> |
|||
<key>CFBundleShortVersionString</key> |
|||
<string>4.0.3</string> |
|||
<key>CFBundleVersion</key> |
|||
<string>239</string> |
|||
<key>LSApplicationCategoryType</key> |
|||
<string></string> |
|||
<key>NSExtension</key> |
|||
<dict> |
|||
<key>NSExtensionAttributes</key> |
|||
<dict> |
|||
<key>WKAppBundleIdentifier</key> |
|||
<string>io.bluewallet.bluewallet.watch</string> |
|||
</dict> |
|||
<key>NSExtensionPointIdentifier</key> |
|||
<string>com.apple.watchkit</string> |
|||
</dict> |
|||
<key>WKExtensionDelegateClassName</key> |
|||
<string>$(PRODUCT_MODULE_NAME).ExtensionDelegate</string> |
|||
</dict> |
|||
</plist> |
@ -0,0 +1,57 @@ |
|||
// |
|||
// InterfaceController.swift |
|||
// BlueWalletWatch Extension |
|||
// |
|||
// Created by Marcos Rodriguez on 3/6/19. |
|||
// Copyright © 2019 Facebook. All rights reserved. |
|||
// |
|||
|
|||
import WatchKit |
|||
import WatchConnectivity |
|||
import Foundation |
|||
|
|||
class InterfaceController: WKInterfaceController { |
|||
|
|||
@IBOutlet weak var walletsTable: WKInterfaceTable! |
|||
@IBOutlet weak var loadingIndicatorGroup: WKInterfaceGroup! |
|||
@IBOutlet weak var noWalletsAvailableLabel: WKInterfaceLabel! |
|||
|
|||
override func willActivate() { |
|||
// This method is called when watch view controller is about to be visible to user |
|||
super.willActivate() |
|||
WCSession.default.sendMessage(["message" : "sendApplicationContext"], replyHandler: nil, errorHandler: nil) |
|||
|
|||
if (WatchDataSource.shared.wallets.isEmpty) { |
|||
loadingIndicatorGroup.setHidden(true) |
|||
noWalletsAvailableLabel.setHidden(false) |
|||
} else { |
|||
processWalletsTable() |
|||
} |
|||
NotificationCenter.default.addObserver(self, selector: #selector(processWalletsTable), name: WatchDataSource.NotificationName.dataUpdated, object: nil) |
|||
} |
|||
|
|||
@objc private func processWalletsTable() { |
|||
loadingIndicatorGroup.setHidden(false) |
|||
walletsTable.setHidden(true) |
|||
walletsTable.setNumberOfRows(WatchDataSource.shared.wallets.count, withRowType: WalletInformation.identifier) |
|||
|
|||
for index in 0..<walletsTable.numberOfRows { |
|||
guard let controller = walletsTable.rowController(at: index) as? WalletInformation else { continue } |
|||
let wallet = WatchDataSource.shared.wallets[index] |
|||
if wallet.identifier == nil { |
|||
WatchDataSource.shared.wallets[index].identifier = index |
|||
} |
|||
controller.name = wallet.label |
|||
controller.balance = wallet.balance |
|||
controller.type = WalletGradient(rawValue: wallet.type) ?? .SegwitHD |
|||
} |
|||
loadingIndicatorGroup.setHidden(true) |
|||
noWalletsAvailableLabel.setHidden(!WatchDataSource.shared.wallets.isEmpty) |
|||
walletsTable.setHidden(WatchDataSource.shared.wallets.isEmpty) |
|||
} |
|||
|
|||
override func contextForSegue(withIdentifier segueIdentifier: String, in table: WKInterfaceTable, rowIndex: Int) -> Any? { |
|||
return rowIndex; |
|||
} |
|||
|
|||
} |
@ -0,0 +1,38 @@ |
|||
// |
|||
// NotificationController.swift |
|||
// BlueWalletWatch Extension |
|||
// |
|||
// Created by Marcos Rodriguez on 3/6/19. |
|||
// Copyright © 2019 Facebook. All rights reserved. |
|||
// |
|||
|
|||
import WatchKit |
|||
import Foundation |
|||
import UserNotifications |
|||
|
|||
|
|||
class NotificationController: WKUserNotificationInterfaceController { |
|||
|
|||
override init() { |
|||
// Initialize variables here. |
|||
super.init() |
|||
|
|||
// Configure interface objects here. |
|||
} |
|||
|
|||
override func willActivate() { |
|||
// This method is called when watch view controller is about to be visible to user |
|||
super.willActivate() |
|||
} |
|||
|
|||
override func didDeactivate() { |
|||
// This method is called when watch view controller is no longer visible |
|||
super.didDeactivate() |
|||
} |
|||
|
|||
override func didReceive(_ notification: UNNotification) { |
|||
// This method is called when a notification needs to be presented. |
|||
// Implement it if you use a dynamic notification interface. |
|||
// Populate your dynamic notification interface as quickly as possible. |
|||
} |
|||
} |
@ -0,0 +1,155 @@ |
|||
// |
|||
// NumericKeypadInterfaceController.swift |
|||
// BlueWalletWatch Extension |
|||
// |
|||
// Created by Marcos Rodriguez on 3/23/19. |
|||
// Copyright © 2019 Facebook. All rights reserved. |
|||
// |
|||
|
|||
import WatchKit |
|||
import Foundation |
|||
|
|||
|
|||
class NumericKeypadInterfaceController: WKInterfaceController { |
|||
|
|||
static let identifier = "NumericKeypadInterfaceController" |
|||
private var amount: [String] = ["0"] |
|||
var keyPadType: NumericKeypadType = .BTC |
|||
struct NotificationName { |
|||
static let keypadDataChanged = Notification.Name(rawValue: "Notification.NumericKeypadInterfaceController.keypadDataChanged") |
|||
} |
|||
struct Notifications { |
|||
static let keypadDataChanged = Notification(name: NotificationName.keypadDataChanged) |
|||
} |
|||
enum NumericKeypadType: String { |
|||
case BTC = "BTC" |
|||
case SATS = "sats" |
|||
} |
|||
|
|||
@IBOutlet weak var periodButton: WKInterfaceButton! |
|||
|
|||
override func awake(withContext context: Any?) { |
|||
super.awake(withContext: context) |
|||
if let context = context as? SpecifyInterfaceController.SpecificQRCodeContent { |
|||
amount = context.amountStringArray |
|||
keyPadType = context.bitcoinUnit |
|||
} |
|||
periodButton.setEnabled(keyPadType == .SATS) |
|||
} |
|||
|
|||
override func willActivate() { |
|||
// This method is called when watch view controller is about to be visible to user |
|||
super.willActivate() |
|||
updateTitle() |
|||
} |
|||
|
|||
private func updateTitle() { |
|||
var title = "" |
|||
for amount in self.amount { |
|||
let isValid = Double(amount) |
|||
if amount == "." || isValid != nil { |
|||
title.append(String(amount)) |
|||
} |
|||
} |
|||
if title.isEmpty { |
|||
title = "0" |
|||
} |
|||
setTitle("< \(title) \(keyPadType)") |
|||
NotificationCenter.default.post(name: NotificationName.keypadDataChanged, object: amount) |
|||
} |
|||
|
|||
private func append(value: String) { |
|||
guard amount.filter({$0 != "."}).count <= 9 && !(amount.contains(".") && value == ".") else { |
|||
return |
|||
} |
|||
switch keyPadType { |
|||
case .SATS: |
|||
if amount.first == "0" { |
|||
if value == "0" { |
|||
return |
|||
} |
|||
amount[0] = value |
|||
} else { |
|||
amount.append(value) |
|||
} |
|||
case .BTC: |
|||
if amount.isEmpty { |
|||
if (value == "0") { |
|||
amount.append("0") |
|||
} else if value == "." && !amount.contains(".") { |
|||
amount.append("0") |
|||
amount.append(".") |
|||
} else { |
|||
amount.append(value) |
|||
} |
|||
} else if let first = amount.first, first == "0" { |
|||
if amount.count > 1, amount[1] != "." { |
|||
amount.insert(".", at: 1) |
|||
} else if amount.count == 1, amount.first == "0" && value != "." { |
|||
amount.append(".") |
|||
amount.append(value) |
|||
} else { |
|||
amount.append(value) |
|||
} |
|||
} else { |
|||
amount.append(value) |
|||
} |
|||
} |
|||
updateTitle() |
|||
} |
|||
|
|||
@IBAction func keypadNumberOneTapped() { |
|||
append(value: "1") |
|||
} |
|||
|
|||
@IBAction func keypadNumberTwoTapped() { |
|||
append(value: "2") |
|||
} |
|||
|
|||
@IBAction func keypadNumberThreeTapped() { |
|||
append(value: "3") |
|||
} |
|||
|
|||
@IBAction func keypadNumberFourTapped() { |
|||
append(value: "4") |
|||
} |
|||
|
|||
@IBAction func keypadNumberFiveTapped() { |
|||
append(value: "5") |
|||
} |
|||
|
|||
@IBAction func keypadNumberSixTapped() { |
|||
append(value: "6") |
|||
} |
|||
|
|||
@IBAction func keypadNumberSevenTapped() { |
|||
append(value: "7") |
|||
} |
|||
|
|||
@IBAction func keypadNumberEightTapped() { |
|||
append(value: "8") |
|||
} |
|||
|
|||
@IBAction func keypadNumberNineTapped() { |
|||
append(value: "9") |
|||
} |
|||
|
|||
@IBAction func keypadNumberZeroTapped() { |
|||
append(value: "0") |
|||
} |
|||
|
|||
@IBAction func keypadNumberDotTapped() { |
|||
guard !amount.contains("."), keyPadType == .BTC else { return } |
|||
append(value: ".") |
|||
} |
|||
|
|||
@IBAction func keypadNumberRemoveTapped() { |
|||
guard !amount.isEmpty else { |
|||
setTitle("< 0 \(keyPadType)") |
|||
return |
|||
} |
|||
amount.removeLast() |
|||
updateTitle() |
|||
} |
|||
|
|||
} |
@ -0,0 +1,39 @@ |
|||
// |
|||
// Wallet.swift |
|||
// BlueWalletWatch Extension |
|||
// |
|||
// Created by Marcos Rodriguez on 3/13/19. |
|||
// Copyright © 2019 Facebook. All rights reserved. |
|||
// |
|||
|
|||
import Foundation |
|||
|
|||
class Transaction: NSObject, NSCoding { |
|||
static let identifier: String = "Transaction" |
|||
|
|||
let time: String |
|||
let memo: String |
|||
let amount: String |
|||
let type: String |
|||
|
|||
init(time: String, memo: String, type: String, amount: String) { |
|||
self.time = time |
|||
self.memo = memo |
|||
self.type = type |
|||
self.amount = amount |
|||
} |
|||
|
|||
func encode(with aCoder: NSCoder) { |
|||
aCoder.encode(time, forKey: "time") |
|||
aCoder.encode(memo, forKey: "memo") |
|||
aCoder.encode(type, forKey: "type") |
|||
aCoder.encode(amount, forKey: "amount") |
|||
} |
|||
|
|||
required init?(coder aDecoder: NSCoder) { |
|||
time = aDecoder.decodeObject(forKey: "time") as! String |
|||
memo = aDecoder.decodeObject(forKey: "memo") as! String |
|||
amount = aDecoder.decodeObject(forKey: "amount") as! String |
|||
type = aDecoder.decodeObject(forKey: "type") as! String |
|||
} |
|||
} |
@ -0,0 +1,52 @@ |
|||
// |
|||
// TransactionTableRow.swift |
|||
// BlueWalletWatch Extension |
|||
// |
|||
// Created by Marcos Rodriguez on 3/10/19. |
|||
// Copyright © 2019 Facebook. All rights reserved. |
|||
// |
|||
|
|||
import WatchKit |
|||
|
|||
class TransactionTableRow: NSObject { |
|||
|
|||
@IBOutlet private weak var transactionAmountLabel: WKInterfaceLabel! |
|||
@IBOutlet private weak var transactionMemoLabel: WKInterfaceLabel! |
|||
@IBOutlet private weak var transactionTimeLabel: WKInterfaceLabel! |
|||
@IBOutlet private weak var transactionTypeImage: WKInterfaceImage! |
|||
|
|||
static let identifier: String = "TransactionTableRow" |
|||
|
|||
var amount: String = "" { |
|||
willSet { |
|||
transactionAmountLabel.setText(newValue) |
|||
} |
|||
} |
|||
|
|||
var memo: String = "" { |
|||
willSet { |
|||
transactionMemoLabel.setText(newValue) |
|||
} |
|||
} |
|||
|
|||
var time: String = "" { |
|||
willSet { |
|||
transactionTimeLabel.setText(newValue) |
|||
} |
|||
} |
|||
|
|||
var type: String = "" { |
|||
willSet { |
|||
if (newValue == "pendingConfirmation") { |
|||
transactionTypeImage.setImage(UIImage(named: "pendingConfirmation")) |
|||
} else if (newValue == "received") { |
|||
transactionTypeImage.setImage(UIImage(named: "receivedArrow")) |
|||
} else if (newValue == "sent") { |
|||
transactionTypeImage.setImage(UIImage(named: "sentArrow")) |
|||
} else { |
|||
transactionTypeImage.setImage(nil) |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,51 @@ |
|||
// |
|||
// Wallet.swift |
|||
// BlueWalletWatch Extension |
|||
// |
|||
// Created by Marcos Rodriguez on 3/13/19. |
|||
// Copyright © 2019 Facebook. All rights reserved. |
|||
// |
|||
|
|||
import Foundation |
|||
|
|||
class Wallet: NSObject, NSCoding { |
|||
static let identifier: String = "Wallet" |
|||
|
|||
var identifier: Int? |
|||
let label: String |
|||
let balance: String |
|||
let type: String |
|||
let preferredBalanceUnit: String |
|||
let receiveAddress: String |
|||
let transactions: [Transaction] |
|||
|
|||
init(label: String, balance: String, type: String, preferredBalanceUnit: String, receiveAddress: String, transactions: [Transaction], identifier: Int) { |
|||
self.label = label |
|||
self.balance = balance |
|||
self.type = type |
|||
self.preferredBalanceUnit = preferredBalanceUnit |
|||
self.receiveAddress = receiveAddress |
|||
self.transactions = transactions |
|||
self.identifier = identifier |
|||
} |
|||
|
|||
func encode(with aCoder: NSCoder) { |
|||
aCoder.encode(label, forKey: "label") |
|||
aCoder.encode(balance, forKey: "balance") |
|||
aCoder.encode(type, forKey: "type") |
|||
aCoder.encode(receiveAddress, forKey: "receiveAddress") |
|||
aCoder.encode(preferredBalanceUnit, forKey: "preferredBalanceUnit") |
|||
aCoder.encode(transactions, forKey: "transactions") |
|||
aCoder.encode(identifier, forKey: "identifier") |
|||
} |
|||
|
|||
required init?(coder aDecoder: NSCoder) { |
|||
label = aDecoder.decodeObject(forKey: "label") as! String |
|||
balance = aDecoder.decodeObject(forKey: "balance") as! String |
|||
type = aDecoder.decodeObject(forKey: "type") as! String |
|||
preferredBalanceUnit = aDecoder.decodeObject(forKey: "preferredBalanceUnit") as! String |
|||
receiveAddress = aDecoder.decodeObject(forKey: "receiveAddress") as! String |
|||
transactions = aDecoder.decodeObject(forKey: "transactions") as? [Transaction] ?? [Transaction]() |
|||
} |
|||
|
|||
} |
@ -0,0 +1,32 @@ |
|||
// |
|||
// WalletGradient.swift |
|||
// BlueWalletWatch Extension |
|||
// |
|||
// Created by Marcos Rodriguez on 3/23/19. |
|||
// Copyright © 2019 Facebook. All rights reserved. |
|||
// |
|||
|
|||
import Foundation |
|||
|
|||
enum WalletGradient: String { |
|||
case SegwitHD = "HDsegwitP2SH" |
|||
case Segwit = "segwitP2SH" |
|||
case LightningCustodial = "lightningCustodianWallet" |
|||
case ACINQStrike = "LightningACINQ" |
|||
case WatchOnly = "watchOnly" |
|||
|
|||
var imageString: String{ |
|||
switch self { |
|||
case .Segwit: |
|||
return "wallet" |
|||
case .ACINQStrike: |
|||
return "walletACINQ" |
|||
case .SegwitHD: |
|||
return "walletHD" |
|||
case .WatchOnly: |
|||
return "walletWatchOnly" |
|||
case .LightningCustodial: |
|||
return "walletLightningCustodial" |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,36 @@ |
|||
// |
|||
// WalletInformation.swift |
|||
// BlueWalletWatch Extension |
|||
// |
|||
// Created by Marcos Rodriguez on 3/10/19. |
|||
// Copyright © 2019 Facebook. All rights reserved. |
|||
// |
|||
|
|||
import WatchKit |
|||
|
|||
class WalletInformation: NSObject { |
|||
|
|||
@IBOutlet private weak var walletBalanceLabel: WKInterfaceLabel! |
|||
@IBOutlet private weak var walletNameLabel: WKInterfaceLabel! |
|||
@IBOutlet private weak var walletGroup: WKInterfaceGroup! |
|||
static let identifier: String = "WalletInformation" |
|||
|
|||
var name: String = "" { |
|||
willSet { |
|||
walletNameLabel.setText(newValue) |
|||
} |
|||
} |
|||
|
|||
var balance: String = "" { |
|||
willSet { |
|||
walletBalanceLabel.setText(newValue) |
|||
} |
|||
} |
|||
|
|||
var type: WalletGradient = .SegwitHD { |
|||
willSet { |
|||
walletGroup.setBackgroundImageNamed(newValue.imageString) |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,103 @@ |
|||
// |
|||
// WatchDataSource.swift |
|||
// BlueWalletWatch Extension |
|||
// |
|||
// Created by Marcos Rodriguez on 3/20/19. |
|||
// Copyright © 2019 Facebook. All rights reserved. |
|||
// |
|||
|
|||
|
|||
import Foundation |
|||
import WatchConnectivity |
|||
|
|||
class WatchDataSource: NSObject, WCSessionDelegate { |
|||
struct NotificationName { |
|||
static let dataUpdated = Notification.Name(rawValue: "Notification.WalletDataSource.Updated") |
|||
} |
|||
struct Notifications { |
|||
static let dataUpdated = Notification(name: NotificationName.dataUpdated) |
|||
} |
|||
|
|||
static let shared = WatchDataSource() |
|||
var wallets: [Wallet] = [Wallet]() |
|||
private let keychain = KeychainSwift() |
|||
|
|||
override init() { |
|||
super.init() |
|||
if WCSession.isSupported() { |
|||
print("Activating watch session") |
|||
WCSession.default.delegate = self |
|||
WCSession.default.activate() |
|||
} |
|||
} |
|||
|
|||
func processWalletsData(walletsInfo: [String: Any]) { |
|||
if let walletsToProcess = walletsInfo["wallets"] as? [[String: Any]] { |
|||
wallets.removeAll(); |
|||
for (index, entry) in walletsToProcess.enumerated() { |
|||
guard let label = entry["label"] as? String, let balance = entry["balance"] as? String, let type = entry["type"] as? String, let preferredBalanceUnit = entry["preferredBalanceUnit"] as? String, let receiveAddress = entry["receiveAddress"] as? String, let transactions = entry["transactions"] as? [[String: Any]] else { |
|||
continue |
|||
} |
|||
var transactionsProcessed = [Transaction]() |
|||
for transactionEntry in transactions { |
|||
guard let time = transactionEntry["time"] as? String, let memo = transactionEntry["memo"] as? String, let amount = transactionEntry["amount"] as? String, let type = transactionEntry["type"] as? String else { continue } |
|||
let transaction = Transaction(time: time, memo: memo, type: type, amount: amount) |
|||
transactionsProcessed.append(transaction) |
|||
} |
|||
let wallet = Wallet(label: label, balance: balance, type: type, preferredBalanceUnit: preferredBalanceUnit, receiveAddress: receiveAddress, transactions: transactionsProcessed, identifier: index) |
|||
wallets.append(wallet) |
|||
} |
|||
|
|||
if let walletsArchived = try? NSKeyedArchiver.archivedData(withRootObject: wallets, requiringSecureCoding: false) { |
|||
keychain.set(walletsArchived, forKey: Wallet.identifier) |
|||
} |
|||
WatchDataSource.postDataUpdatedNotification() |
|||
} |
|||
} |
|||
|
|||
static func postDataUpdatedNotification() { |
|||
NotificationCenter.default.post(Notifications.dataUpdated) |
|||
} |
|||
|
|||
static func requestLightningInvoice(walletIdentifier: Int, amount: Double, description: String?, responseHandler: @escaping (_ invoice: String) -> Void) { |
|||
guard WatchDataSource.shared.wallets.count > walletIdentifier else { |
|||
responseHandler("") |
|||
return |
|||
} |
|||
WCSession.default.sendMessage(["request": "createInvoice", "walletIndex": walletIdentifier, "amount": amount, "description": description ?? ""], replyHandler: { (reply: [String : Any]) in |
|||
if let invoicePaymentRequest = reply["invoicePaymentRequest"] as? String, !invoicePaymentRequest.isEmpty { |
|||
responseHandler(invoicePaymentRequest) |
|||
} else { |
|||
responseHandler("") |
|||
} |
|||
}) { (error) in |
|||
print(error) |
|||
responseHandler("") |
|||
|
|||
} |
|||
} |
|||
|
|||
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) { |
|||
WatchDataSource.shared.processWalletsData(walletsInfo: applicationContext) |
|||
} |
|||
|
|||
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) { |
|||
WatchDataSource.shared.processWalletsData(walletsInfo: applicationContext) |
|||
} |
|||
|
|||
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) { |
|||
// WatchDataSource.shared.processWalletsData(walletsInfo: userInfo) |
|||
} |
|||
|
|||
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { |
|||
if activationState == .activated { |
|||
WCSession.default.sendMessage([:], replyHandler: nil, errorHandler: nil) |
|||
if let existingData = keychain.getData(Wallet.identifier), let walletData = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(existingData) as? [Wallet] { |
|||
guard let walletData = walletData, walletData != self.wallets else { return } |
|||
wallets = walletData |
|||
WatchDataSource.postDataUpdatedNotification() |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,20 @@ |
|||
{ |
|||
"aps": { |
|||
"alert": { |
|||
"body": "Test message", |
|||
"title": "Optional title", |
|||
"subtitle": "Optional subtitle" |
|||
}, |
|||
"category": "myCategory", |
|||
"thread-id":"5280" |
|||
}, |
|||
|
|||
"WatchKit Simulator Actions": [ |
|||
{ |
|||
"title": "First Button", |
|||
"identifier": "firstButtonAction" |
|||
} |
|||
], |
|||
|
|||
"customKey": "Use this file to define a testing payload for your notifications. The aps dictionary specifies the category, alert text and title. The WatchKit Simulator Actions array can provide info for one or more action buttons in addition to the standard Dismiss button. Any other top level keys are custom payload. If you have multiple such JSON files in your project, you'll be able to select them when choosing to debug the notification interface of your Watch App." |
|||
} |
@ -0,0 +1,115 @@ |
|||
// |
|||
// ReceiveInterfaceController.swift |
|||
// BlueWalletWatch Extension |
|||
// |
|||
// Created by Marcos Rodriguez on 3/12/19. |
|||
// Copyright © 2019 Facebook. All rights reserved. |
|||
// |
|||
|
|||
import WatchKit |
|||
import Foundation |
|||
import EFQRCode |
|||
|
|||
class ReceiveInterfaceController: WKInterfaceController { |
|||
|
|||
static let identifier = "ReceiveInterfaceController" |
|||
@IBOutlet weak var imageInterface: WKInterfaceImage! |
|||
private var wallet: Wallet? |
|||
private var isRenderingQRCode: Bool? |
|||
@IBOutlet weak var loadingIndicator: WKInterfaceGroup! |
|||
|
|||
override func awake(withContext context: Any?) { |
|||
super.awake(withContext: context) |
|||
guard let identifier = context as? Int, WatchDataSource.shared.wallets.count > identifier else { |
|||
pop() |
|||
return |
|||
} |
|||
let wallet = WatchDataSource.shared.wallets[identifier] |
|||
self.wallet = wallet |
|||
NotificationCenter.default.addObserver(forName: SpecifyInterfaceController.NotificationName.createQRCode, object: nil, queue: nil) { [weak self] (notification) in |
|||
self?.isRenderingQRCode = true |
|||
if let wallet = self?.wallet, wallet.type == "lightningCustodianWallet", let object = notification.object as? SpecifyInterfaceController.SpecificQRCodeContent, let amount = object.amount { |
|||
self?.imageInterface.setHidden(true) |
|||
self?.loadingIndicator.setHidden(false) |
|||
WatchDataSource.requestLightningInvoice(walletIdentifier: identifier, amount: amount, description: object.description, responseHandler: { (invoice) in |
|||
DispatchQueue.main.async { |
|||
if (!invoice.isEmpty) { |
|||
guard let cgImage = EFQRCode.generate( |
|||
content: "lightning:\(invoice)") else { |
|||
return |
|||
} |
|||
let image = UIImage(cgImage: cgImage) |
|||
self?.loadingIndicator.setHidden(true) |
|||
self?.imageInterface.setHidden(false) |
|||
self?.imageInterface.setImage(nil) |
|||
self?.imageInterface.setImage(image) |
|||
} else { |
|||
self?.pop() |
|||
self?.presentAlert(withTitle: "Error", message: "Unable to create invoice. Please, make sure your iPhone is paired and nearby.", preferredStyle: .alert, actions: [WKAlertAction(title: "OK", style: .default, handler: { [weak self] in |
|||
self?.dismiss() |
|||
})]) |
|||
} |
|||
} |
|||
}) |
|||
} else { |
|||
guard let notificationObject = notification.object as? SpecifyInterfaceController.SpecificQRCodeContent, let walletContext = self?.wallet, !walletContext.receiveAddress.isEmpty, let receiveAddress = self?.wallet?.receiveAddress else { return } |
|||
var address = "bitcoin:\(receiveAddress)" |
|||
|
|||
var hasAmount = false |
|||
if let amount = notificationObject.amount { |
|||
address.append("?amount=\(amount)&") |
|||
hasAmount = true |
|||
} |
|||
if let description = notificationObject.description { |
|||
if (!hasAmount) { |
|||
address.append("?") |
|||
} |
|||
address.append("label=\(description)") |
|||
} |
|||
|
|||
DispatchQueue.main.async { |
|||
guard let cgImage = EFQRCode.generate( |
|||
content: address) else { |
|||
return |
|||
} |
|||
let image = UIImage(cgImage: cgImage) |
|||
self?.imageInterface.setImage(nil) |
|||
self?.imageInterface.setImage(image) |
|||
self?.imageInterface.setHidden(false) |
|||
self?.loadingIndicator.setHidden(true) |
|||
self?.isRenderingQRCode = false |
|||
} |
|||
} |
|||
} |
|||
|
|||
guard !wallet.receiveAddress.isEmpty, let cgImage = EFQRCode.generate( |
|||
content: wallet.receiveAddress) else { |
|||
return |
|||
} |
|||
|
|||
let image = UIImage(cgImage: cgImage) |
|||
imageInterface.setImage(image) |
|||
} |
|||
|
|||
override func didAppear() { |
|||
super.didAppear() |
|||
if wallet?.type == "lightningCustodianWallet" { |
|||
if isRenderingQRCode == nil { |
|||
presentController(withName: SpecifyInterfaceController.identifier, context: wallet?.identifier) |
|||
isRenderingQRCode = false |
|||
} else if isRenderingQRCode == false { |
|||
pop() |
|||
} |
|||
} |
|||
} |
|||
|
|||
override func didDeactivate() { |
|||
super.didDeactivate() |
|||
NotificationCenter.default.removeObserver(self, name: SpecifyInterfaceController.NotificationName.createQRCode, object: nil) |
|||
} |
|||
|
|||
@IBAction func specifyMenuItemTapped() { |
|||
presentController(withName: SpecifyInterfaceController.identifier, context: wallet?.identifier) |
|||
} |
|||
|
|||
} |
@ -0,0 +1,90 @@ |
|||
// |
|||
// SpecifyInterfaceController.swift |
|||
// BlueWalletWatch Extension |
|||
// |
|||
// Created by Marcos Rodriguez on 3/23/19. |
|||
// Copyright © 2019 Facebook. All rights reserved. |
|||
// |
|||
|
|||
import WatchKit |
|||
import Foundation |
|||
|
|||
class SpecifyInterfaceController: WKInterfaceController { |
|||
|
|||
static let identifier = "SpecifyInterfaceController" |
|||
@IBOutlet weak var descriptionButton: WKInterfaceButton! |
|||
@IBOutlet weak var amountButton: WKInterfaceButton! |
|||
struct SpecificQRCodeContent { |
|||
var amount: Double? |
|||
var description: String? |
|||
var amountStringArray: [String] = ["0"] |
|||
var bitcoinUnit: NumericKeypadInterfaceController.NumericKeypadType = .BTC |
|||
} |
|||
var specifiedQRContent: SpecificQRCodeContent = SpecificQRCodeContent(amount: nil, description: nil, amountStringArray: ["0"], bitcoinUnit: .BTC) |
|||
var wallet: Wallet? |
|||
struct NotificationName { |
|||
static let createQRCode = Notification.Name(rawValue: "Notification.SpecifyInterfaceController.createQRCode") |
|||
} |
|||
struct Notifications { |
|||
static let createQRCode = Notification(name: NotificationName.createQRCode) |
|||
} |
|||
|
|||
override func awake(withContext context: Any?) { |
|||
super.awake(withContext: context) |
|||
guard let identifier = context as? Int, WatchDataSource.shared.wallets.count > identifier else { |
|||
return |
|||
} |
|||
let wallet = WatchDataSource.shared.wallets[identifier] |
|||
self.wallet = wallet |
|||
self.specifiedQRContent.bitcoinUnit = wallet.type == "lightningCustodianWallet" ? .SATS : .BTC |
|||
NotificationCenter.default.addObserver(forName: NumericKeypadInterfaceController.NotificationName.keypadDataChanged, object: nil, queue: nil) { [weak self] (notification) in |
|||
guard let amountObject = notification.object as? [String], !amountObject.isEmpty else { return } |
|||
if amountObject.count == 1 && (amountObject.first == "." || amountObject.first == "0") { |
|||
return |
|||
} |
|||
var title = "" |
|||
for amount in amountObject { |
|||
let isValid = Double(amount) |
|||
if amount == "." || isValid != nil { |
|||
title.append(String(amount)) |
|||
} |
|||
} |
|||
self?.specifiedQRContent.amountStringArray = amountObject |
|||
if let amountDouble = Double(title), let keyPadType = self?.specifiedQRContent.bitcoinUnit { |
|||
self?.specifiedQRContent.amount = amountDouble |
|||
self?.amountButton.setTitle("\(title) \(keyPadType)") |
|||
} |
|||
} |
|||
} |
|||
|
|||
override func didDeactivate() { |
|||
// This method is called when watch view controller is no longer visible |
|||
super.didDeactivate() |
|||
NotificationCenter.default.removeObserver(self, name: NumericKeypadInterfaceController.NotificationName.keypadDataChanged, object: nil) |
|||
} |
|||
|
|||
@IBAction func descriptionButtonTapped() { |
|||
presentTextInputController(withSuggestions: nil, allowedInputMode: .allowEmoji) { [weak self] (result: [Any]?) in |
|||
DispatchQueue.main.async { |
|||
if let result = result, let text = result.first as? String { |
|||
self?.specifiedQRContent.description = text |
|||
self?.descriptionButton.setTitle(nil) |
|||
self?.descriptionButton.setTitle(text) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@IBAction func createButtonTapped() { |
|||
NotificationCenter.default.post(name: NotificationName.createQRCode, object: specifiedQRContent) |
|||
dismiss() |
|||
} |
|||
|
|||
override func contextForSegue(withIdentifier segueIdentifier: String) -> Any? { |
|||
if segueIdentifier == NumericKeypadInterfaceController.identifier { |
|||
return specifiedQRContent |
|||
} |
|||
return nil |
|||
} |
|||
|
|||
} |
@ -0,0 +1,68 @@ |
|||
// |
|||
// WalletDetailsInterfaceController.swift |
|||
// BlueWalletWatch Extension |
|||
// |
|||
// Created by Marcos Rodriguez on 3/11/19. |
|||
// Copyright © 2019 Facebook. All rights reserved. |
|||
// |
|||
|
|||
import WatchKit |
|||
import Foundation |
|||
|
|||
|
|||
class WalletDetailsInterfaceController: WKInterfaceController { |
|||
|
|||
var wallet: Wallet? |
|||
static let identifier = "WalletDetailsInterfaceController" |
|||
@IBOutlet weak var walletBasicsGroup: WKInterfaceGroup! |
|||
@IBOutlet weak var walletBalanceLabel: WKInterfaceLabel! |
|||
@IBOutlet weak var walletNameLabel: WKInterfaceLabel! |
|||
@IBOutlet weak var receiveButton: WKInterfaceButton! |
|||
@IBOutlet weak var noTransactionsLabel: WKInterfaceLabel! |
|||
@IBOutlet weak var transactionsTable: WKInterfaceTable! |
|||
|
|||
override func awake(withContext context: Any?) { |
|||
super.awake(withContext: context) |
|||
guard let identifier = context as? Int else { |
|||
pop() |
|||
return |
|||
} |
|||
let wallet = WatchDataSource.shared.wallets[identifier] |
|||
self.wallet = wallet |
|||
walletBalanceLabel.setText(wallet.balance) |
|||
walletNameLabel.setText(wallet.label) |
|||
walletBasicsGroup.setBackgroundImageNamed(WalletGradient(rawValue: wallet.type)?.imageString) |
|||
|
|||
processWalletsTable() |
|||
} |
|||
|
|||
override func willActivate() { |
|||
super.willActivate() |
|||
transactionsTable.setHidden(wallet?.transactions.isEmpty ?? true) |
|||
noTransactionsLabel.setHidden(!(wallet?.transactions.isEmpty ?? false)) |
|||
} |
|||
|
|||
@IBAction func receiveMenuItemTapped() { |
|||
presentController(withName: ReceiveInterfaceController.identifier, context: wallet) |
|||
} |
|||
|
|||
@objc private func processWalletsTable() { |
|||
transactionsTable.setNumberOfRows(wallet?.transactions.count ?? 0, withRowType: TransactionTableRow.identifier) |
|||
|
|||
for index in 0..<transactionsTable.numberOfRows { |
|||
guard let controller = transactionsTable.rowController(at: index) as? TransactionTableRow, let transaction = wallet?.transactions[index] else { continue } |
|||
|
|||
controller.amount = transaction.amount |
|||
controller.type = transaction.type |
|||
controller.memo = transaction.memo |
|||
controller.time = transaction.time |
|||
} |
|||
transactionsTable.setHidden(wallet?.transactions.isEmpty ?? true) |
|||
noTransactionsLabel.setHidden(!(wallet?.transactions.isEmpty ?? false)) |
|||
} |
|||
|
|||
override func contextForSegue(withIdentifier segueIdentifier: String) -> Any? { |
|||
return wallet?.identifier |
|||
} |
|||
|
|||
} |
After Width: | Height: | Size: 193 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 2.2 KiB |
@ -0,0 +1,92 @@ |
|||
{ |
|||
"images" : [ |
|||
{ |
|||
"size" : "24x24", |
|||
"idiom" : "watch", |
|||
"filename" : "Icon-48.png", |
|||
"scale" : "2x", |
|||
"role" : "notificationCenter", |
|||
"subtype" : "38mm" |
|||
}, |
|||
{ |
|||
"size" : "27.5x27.5", |
|||
"idiom" : "watch", |
|||
"filename" : "Icon-55.png", |
|||
"scale" : "2x", |
|||
"role" : "notificationCenter", |
|||
"subtype" : "42mm" |
|||
}, |
|||
{ |
|||
"size" : "29x29", |
|||
"idiom" : "watch", |
|||
"filename" : "58.png", |
|||
"role" : "companionSettings", |
|||
"scale" : "2x" |
|||
}, |
|||
{ |
|||
"size" : "29x29", |
|||
"idiom" : "watch", |
|||
"filename" : "87.png", |
|||
"role" : "companionSettings", |
|||
"scale" : "3x" |
|||
}, |
|||
{ |
|||
"size" : "40x40", |
|||
"idiom" : "watch", |
|||
"filename" : "watch.png", |
|||
"scale" : "2x", |
|||
"role" : "appLauncher", |
|||
"subtype" : "38mm" |
|||
}, |
|||
{ |
|||
"size" : "44x44", |
|||
"idiom" : "watch", |
|||
"filename" : "Icon-88.png", |
|||
"scale" : "2x", |
|||
"role" : "appLauncher", |
|||
"subtype" : "40mm" |
|||
}, |
|||
{ |
|||
"size" : "50x50", |
|||
"idiom" : "watch", |
|||
"filename" : "Icon-173.png", |
|||
"scale" : "2x", |
|||
"role" : "appLauncher", |
|||
"subtype" : "44mm" |
|||
}, |
|||
{ |
|||
"size" : "86x86", |
|||
"idiom" : "watch", |
|||
"filename" : "Icon-172.png", |
|||
"scale" : "2x", |
|||
"role" : "quickLook", |
|||
"subtype" : "38mm" |
|||
}, |
|||
{ |
|||
"size" : "98x98", |
|||
"idiom" : "watch", |
|||
"filename" : "Icon-196.png", |
|||
"scale" : "2x", |
|||
"role" : "quickLook", |
|||
"subtype" : "42mm" |
|||
}, |
|||
{ |
|||
"size" : "108x108", |
|||
"idiom" : "watch", |
|||
"filename" : "group-copy-2@3x.png", |
|||
"scale" : "2x", |
|||
"role" : "quickLook", |
|||
"subtype" : "44mm" |
|||
}, |
|||
{ |
|||
"size" : "1024x1024", |
|||
"idiom" : "watch-marketing", |
|||
"filename" : "1024.png", |
|||
"scale" : "1x" |
|||
} |
|||
], |
|||
"info" : { |
|||
"version" : 1, |
|||
"author" : "xcode" |
|||
} |
|||
} |
After Width: | Height: | Size: 7.5 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 9.0 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 9.4 KiB |
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,6 @@ |
|||
{ |
|||
"info" : { |
|||
"version" : 1, |
|||
"author" : "xcode" |
|||
} |
|||
} |
@ -0,0 +1,13 @@ |
|||
{ |
|||
"images" : [ |
|||
{ |
|||
"idiom" : "watch", |
|||
"filename" : "group-copy-2@3x.png", |
|||
"scale" : "2x" |
|||
} |
|||
], |
|||
"info" : { |
|||
"version" : 1, |
|||
"author" : "xcode" |
|||
} |
|||
} |
After Width: | Height: | Size: 14 KiB |
@ -0,0 +1,13 @@ |
|||
{ |
|||
"images" : [ |
|||
{ |
|||
"idiom" : "watch", |
|||
"filename" : "shape@3x.png", |
|||
"scale" : "2x" |
|||
} |
|||
], |
|||
"info" : { |
|||
"version" : 1, |
|||
"author" : "xcode" |
|||
} |
|||
} |
After Width: | Height: | Size: 692 B |
@ -0,0 +1,13 @@ |
|||
{ |
|||
"images" : [ |
|||
{ |
|||
"idiom" : "watch", |
|||
"filename" : "qr-code@3x.png", |
|||
"scale" : "2x" |
|||
} |
|||
], |
|||
"info" : { |
|||
"version" : 1, |
|||
"author" : "xcode" |
|||
} |
|||
} |
After Width: | Height: | Size: 112 KiB |
@ -0,0 +1,13 @@ |
|||
{ |
|||
"images" : [ |
|||
{ |
|||
"idiom" : "watch", |
|||
"filename" : "path-copy-3@2x.png", |
|||
"scale" : "2x" |
|||
} |
|||
], |
|||
"info" : { |
|||
"version" : 1, |
|||
"author" : "xcode" |
|||
} |
|||
} |
After Width: | Height: | Size: 447 B |
@ -0,0 +1,13 @@ |
|||
{ |
|||
"images" : [ |
|||
{ |
|||
"idiom" : "watch", |
|||
"filename" : "path-copy@2x.png", |
|||
"scale" : "2x" |
|||
} |
|||
], |
|||
"info" : { |
|||
"version" : 1, |
|||
"author" : "xcode" |
|||
} |
|||
} |
After Width: | Height: | Size: 471 B |
@ -0,0 +1,23 @@ |
|||
{ |
|||
"images" : [ |
|||
{ |
|||
"idiom" : "universal", |
|||
"filename" : "mask.png", |
|||
"scale" : "1x" |
|||
}, |
|||
{ |
|||
"idiom" : "universal", |
|||
"filename" : "mask@2x.png", |
|||
"scale" : "2x" |
|||
}, |
|||
{ |
|||
"idiom" : "universal", |
|||
"filename" : "mask@3x.png", |
|||
"scale" : "3x" |
|||
} |
|||
], |
|||
"info" : { |
|||
"version" : 1, |
|||
"author" : "xcode" |
|||
} |
|||
} |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 25 KiB |
@ -0,0 +1,23 @@ |
|||
{ |
|||
"images" : [ |
|||
{ |
|||
"idiom" : "universal", |
|||
"filename" : "mask.png", |
|||
"scale" : "1x" |
|||
}, |
|||
{ |
|||
"idiom" : "universal", |
|||
"filename" : "mask@2x.png", |
|||
"scale" : "2x" |
|||
}, |
|||
{ |
|||
"idiom" : "universal", |
|||
"filename" : "mask@3x.png", |
|||
"scale" : "3x" |
|||
} |
|||
], |
|||
"info" : { |
|||
"version" : 1, |
|||
"author" : "xcode" |
|||
} |
|||
} |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 25 KiB |
@ -0,0 +1,13 @@ |
|||
{ |
|||
"images" : [ |
|||
{ |
|||
"idiom" : "watch", |
|||
"filename" : "mask@3x.png", |
|||
"scale" : "2x" |
|||
} |
|||
], |
|||
"info" : { |
|||
"version" : 1, |
|||
"author" : "xcode" |
|||
} |
|||
} |
After Width: | Height: | Size: 25 KiB |
@ -0,0 +1,13 @@ |
|||
{ |
|||
"images" : [ |
|||
{ |
|||
"idiom" : "watch", |
|||
"filename" : "mask@3x.png", |
|||
"scale" : "2x" |
|||
} |
|||
], |
|||
"info" : { |
|||
"version" : 1, |
|||
"author" : "xcode" |
|||
} |
|||
} |