From 6904c2759ec75ab9c3a5ace358a20c582dfa1a1b Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 8 May 2019 04:53:26 +0200 Subject: [PATCH] implement payto URIs --- src/dbTypes.ts | 17 ++---- src/i18n/de.po | 2 +- src/i18n/en-US.po | 2 +- src/i18n/fr.po | 2 +- src/i18n/it.po | 2 +- src/i18n/sv.po | 2 +- src/i18n/taler-wallet-webex.pot | 2 +- src/talerTypes.ts | 42 ++++++--------- src/wallet.ts | 62 +++++++++------------- src/walletTypes.ts | 23 +++----- src/webex/pages/confirm-create-reserve.tsx | 40 ++++++++------ src/webex/wxApi.ts | 2 +- webpack.config.js | 2 +- 13 files changed, 82 insertions(+), 118 deletions(-) diff --git a/src/dbTypes.ts b/src/dbTypes.ts index 6369cd92a..3cb9a0d32 100644 --- a/src/dbTypes.ts +++ b/src/dbTypes.ts @@ -34,7 +34,6 @@ import { MerchantRefundPermission, PayReq, TipResponse, - WireDetail, } from "./talerTypes"; import { @@ -114,10 +113,10 @@ export interface ReserveRecord { hasPayback: boolean; /** - * Wire information for the bank account that + * Wire information (as payto URI) for the bank account that * transfered funds for this reserve. */ - senderWire?: object; + senderWire?: string; } @@ -837,15 +836,7 @@ export interface PurchaseRecord { * Information about wire information for bank accounts we withdrew coins from. */ export interface SenderWireRecord { - /** - * Wire details. - */ - senderWire: WireDetail; - - /** - * Identifier, hash code of canonicalized senderWire. - */ - id: number; + paytoUri: string; } @@ -1001,7 +992,7 @@ export namespace Stores { class SenderWiresStore extends Store { constructor() { - super("senderWires", { keyPath: "id" }); + super("senderWires", { keyPath: "paytoUri" }); } } diff --git a/src/i18n/de.po b/src/i18n/de.po index ea98099c3..70d7069cd 100644 --- a/src/i18n/de.po +++ b/src/i18n/de.po @@ -235,7 +235,7 @@ msgstr "" #. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-# #. TODO:generic error reporting function or component. -#: src/webex/pages/confirm-create-reserve.tsx:511 src/webex/pages/tip.tsx:180 +#: src/webex/pages/confirm-create-reserve.tsx:515 src/webex/pages/tip.tsx:180 #, c-format msgid "Fatal error: \"%1$s\"." msgstr "" diff --git a/src/i18n/en-US.po b/src/i18n/en-US.po index 5d68c6167..2bd3e0f59 100644 --- a/src/i18n/en-US.po +++ b/src/i18n/en-US.po @@ -235,7 +235,7 @@ msgstr "" #. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-# #. TODO:generic error reporting function or component. -#: src/webex/pages/confirm-create-reserve.tsx:511 src/webex/pages/tip.tsx:180 +#: src/webex/pages/confirm-create-reserve.tsx:515 src/webex/pages/tip.tsx:180 #, c-format msgid "Fatal error: \"%1$s\"." msgstr "" diff --git a/src/i18n/fr.po b/src/i18n/fr.po index f097767a8..e62328a9c 100644 --- a/src/i18n/fr.po +++ b/src/i18n/fr.po @@ -235,7 +235,7 @@ msgstr "" #. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-# #. TODO:generic error reporting function or component. -#: src/webex/pages/confirm-create-reserve.tsx:511 src/webex/pages/tip.tsx:180 +#: src/webex/pages/confirm-create-reserve.tsx:515 src/webex/pages/tip.tsx:180 #, c-format msgid "Fatal error: \"%1$s\"." msgstr "" diff --git a/src/i18n/it.po b/src/i18n/it.po index f097767a8..e62328a9c 100644 --- a/src/i18n/it.po +++ b/src/i18n/it.po @@ -235,7 +235,7 @@ msgstr "" #. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-# #. TODO:generic error reporting function or component. -#: src/webex/pages/confirm-create-reserve.tsx:511 src/webex/pages/tip.tsx:180 +#: src/webex/pages/confirm-create-reserve.tsx:515 src/webex/pages/tip.tsx:180 #, c-format msgid "Fatal error: \"%1$s\"." msgstr "" diff --git a/src/i18n/sv.po b/src/i18n/sv.po index 902f5cf1f..36cde50c7 100644 --- a/src/i18n/sv.po +++ b/src/i18n/sv.po @@ -239,7 +239,7 @@ msgstr "" #. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-# #. TODO:generic error reporting function or component. -#: src/webex/pages/confirm-create-reserve.tsx:511 src/webex/pages/tip.tsx:180 +#: src/webex/pages/confirm-create-reserve.tsx:515 src/webex/pages/tip.tsx:180 #, c-format msgid "Fatal error: \"%1$s\"." msgstr "" diff --git a/src/i18n/taler-wallet-webex.pot b/src/i18n/taler-wallet-webex.pot index f097767a8..e62328a9c 100644 --- a/src/i18n/taler-wallet-webex.pot +++ b/src/i18n/taler-wallet-webex.pot @@ -235,7 +235,7 @@ msgstr "" #. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-# #. TODO:generic error reporting function or component. -#: src/webex/pages/confirm-create-reserve.tsx:511 src/webex/pages/tip.tsx:180 +#: src/webex/pages/confirm-create-reserve.tsx:515 src/webex/pages/tip.tsx:180 #, c-format msgid "Fatal error: \"%1$s\"." msgstr "" diff --git a/src/talerTypes.ts b/src/talerTypes.ts index f8fb72b93..db49b0747 100644 --- a/src/talerTypes.ts +++ b/src/talerTypes.ts @@ -852,29 +852,25 @@ export class WireFeesJson { } -/** - * Information about wire transfer methods supported - * by the exchange. - */ -@Checkable.Class({extra: true}) -export class WireDetailJson { - /** - * Name of the wire transfer method. - */ +@Checkable.Class() +export class AccountInfo { @Checkable.String() - type: string; + url: string; - /** - * Fees associated with the wire transfer method. - */ - @Checkable.List(Checkable.Value(() => WireFeesJson)) - fees: WireFeesJson[]; + @Checkable.String() + master_sig: string; +} - /** - * Verify that a value matches the schema of this class and convert it into a - * member. - */ - static checked: (obj: any) => WireDetailJson; + +@Checkable.Class({extra: true}) +export class ExchangeWireJson { + @Checkable.Map(Checkable.String(), Checkable.List(Checkable.Value(() => WireFeesJson))) + fees: { [methodName: string]: WireFeesJson[] }; + + @Checkable.List(Checkable.Value(() => AccountInfo)) + accounts: AccountInfo[]; + + static checked: (obj: any) => ExchangeWireJson; } @@ -884,12 +880,6 @@ export class WireDetailJson { */ export type WireDetail = object & { type: string }; -/** - * Type guard for wire details. - */ -export function isWireDetail(x: any): x is WireDetail { - return x && typeof x === "object" && typeof x.type === "string"; -} /** * Proposal returned from the contract URL. diff --git a/src/wallet.ts b/src/wallet.ts index 434eb8b8c..58c69d79c 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -28,7 +28,6 @@ import { canonicalJson, canonicalizeBaseUrl, getTalerStampSec, - hash, strcmp, } from "./helpers"; import { @@ -75,6 +74,7 @@ import { ContractTerms, Denomination, ExchangeHandle, + ExchangeWireJson, KeysJson, MerchantRefundPermission, MerchantRefundResponse, @@ -86,8 +86,6 @@ import { TipPlanchetDetail, TipResponse, TipToken, - WireDetailJson, - isWireDetail, } from "./talerTypes"; import { Badge, @@ -109,7 +107,6 @@ import { TipStatus, WalletBalance, WalletBalanceEntry, - WireInfo, } from "./walletTypes"; @@ -1133,10 +1130,9 @@ export class Wallet { }; const senderWire = req.senderWire; - if (isWireDetail(senderWire)) { + if (senderWire) { const rec = { - id: hash(senderWire), - senderWire, + paytoUri: senderWire, }; await this.q().put(Stores.senderWires, rec).finish(); } @@ -1327,7 +1323,7 @@ export class Wallet { /** * Get the wire information for the exchange with the given base URL. */ - async getWireInfo(exchangeBaseUrl: string): Promise { + async getWireInfo(exchangeBaseUrl: string): Promise { exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl); const reqUrl = new URI("wire").absoluteTo(exchangeBaseUrl); const resp = await this.http.get(reqUrl.href()); @@ -1340,7 +1336,7 @@ export class Wallet { if (!wiJson) { throw Error("/wire response malformed"); } - return wiJson; + return ExchangeWireJson.checked(wiJson) } @@ -1500,6 +1496,11 @@ export class Wallet { throw Error(`no wire fees found for exchange ${baseUrl}`); } + const exchangeWireAccounts: string[] = []; + for (let account of wireInfo.accounts) { + exchangeWireAccounts.push(account.url); + } + const {isTrusted, isAudited} = await this.getExchangeTrust(exchangeInfo); let earliestDepositExpiration = Infinity; @@ -1534,10 +1535,10 @@ export class Wallet { } } - const ret: ReserveCreationInfo = { earliestDepositExpiration, exchangeInfo, + exchangeWireAccounts, exchangeVersion: exchangeInfo.protocolVersion || "unknown", isAudited, isTrusted, @@ -1548,7 +1549,6 @@ export class Wallet { versionMatch, walletVersion: WALLET_PROTOCOL_VERSION, wireFees, - wireInfo, withdrawFee: acc, }; return ret; @@ -1563,26 +1563,13 @@ export class Wallet { async updateExchangeFromUrl(baseUrl: string): Promise { baseUrl = canonicalizeBaseUrl(baseUrl); const keysUrl = new URI("keys").absoluteTo(baseUrl); - const wireUrl = new URI("wire").absoluteTo(baseUrl); const keysResp = await this.http.get(keysUrl.href()); if (keysResp.status !== 200) { throw Error("/keys request failed"); } - const wireResp = await this.http.get(wireUrl.href()); - if (wireResp.status !== 200) { - throw Error("/wire request failed"); - } const exchangeKeysJson = KeysJson.checked(JSON.parse(keysResp.responseText)); - const wireRespJson = JSON.parse(wireResp.responseText); - if (typeof wireRespJson !== "object") { - throw Error("/wire response is not an object"); - } - console.log("exchange wire", wireRespJson); - const wireMethodDetails: WireDetailJson[] = []; - for (const methodName in wireRespJson) { - wireMethodDetails.push(WireDetailJson.checked(wireRespJson[methodName])); - } - return this.updateExchangeFromJson(baseUrl, exchangeKeysJson, wireMethodDetails); + const exchangeWire = await this.getWireInfo(baseUrl); + return this.updateExchangeFromJson(baseUrl, exchangeKeysJson, exchangeWire); } @@ -1614,7 +1601,7 @@ export class Wallet { private async updateExchangeFromJson(baseUrl: string, exchangeKeysJson: KeysJson, - wireMethodDetails: WireDetailJson[]): Promise { + wireMethodDetails: ExchangeWireJson): Promise { // FIXME: all this should probably be commited atomically const updateTimeSec = getTalerStampSec(exchangeKeysJson.list_issue_date); @@ -1667,16 +1654,17 @@ export class Wallet { }; } - for (const detail of wireMethodDetails) { + for (const paytoTargetType in wireMethodDetails.fees) { let latestFeeStamp = 0; - const fees = oldWireFees.feesForType[detail.type] || []; - oldWireFees.feesForType[detail.type] = fees; - for (const oldFee of fees) { + const newFeeDetails = wireMethodDetails.fees[paytoTargetType]; + const oldFeeDetails = oldWireFees.feesForType[paytoTargetType] || []; + oldWireFees.feesForType[paytoTargetType] = oldFeeDetails; + for (const oldFee of oldFeeDetails) { if (oldFee.endStamp > latestFeeStamp) { latestFeeStamp = oldFee.endStamp; } } - for (const fee of detail.fees) { + for (const fee of newFeeDetails) { const start = getTalerStampSec(fee.start_date); if (start === null) { console.error("invalid start stamp in fee", fee); @@ -1697,12 +1685,12 @@ export class Wallet { startStamp: start, wireFee: Amounts.parseOrThrow(fee.wire_fee), }; - const valid: boolean = await this.cryptoApi.isValidWireFee(detail.type, wf, exchangeInfo.masterPublicKey); + const valid: boolean = await this.cryptoApi.isValidWireFee(paytoTargetType, wf, exchangeInfo.masterPublicKey); if (!valid) { console.error("fee signature invalid", fee); throw Error("fee signature invalid"); } - fees.push(wf); + oldFeeDetails.push(wf); } } @@ -2434,11 +2422,9 @@ export class Wallet { const senderWiresSet = new Set(); await this.q().iter(Stores.senderWires).map((x) => { - if (x.senderWire) { - senderWiresSet.add(canonicalJson(x.senderWire)); - } + senderWiresSet.add(x.paytoUri); }).run(); - const senderWires = Array.from(senderWiresSet).map((x) => JSON.parse(x)); + const senderWires = Array.from(senderWiresSet); return { exchangeWireTypes, diff --git a/src/walletTypes.ts b/src/walletTypes.ts index b6355db0d..73a72bbbf 100644 --- a/src/walletTypes.ts +++ b/src/walletTypes.ts @@ -70,19 +70,6 @@ export class CreateReserveResponse { } -/** - * Wire info, sent to the bank when creating a reserve. Fee information will - * be filtered out. Only methods that the bank also supports should be sent. - */ -export interface WireInfo { - /** - * Mapping from wire method type to the exchange's wire info, - * excluding fees. - */ - [type: string]: any; -} - - /** * Information about what will happen when creating a reserve. * @@ -97,7 +84,7 @@ export interface ReserveCreationInfo { /** * Filtered wire info to send to the bank. */ - wireInfo: WireInfo; + exchangeWireAccounts: string[]; /** * Selected denominations for withdraw. @@ -139,6 +126,7 @@ export interface ReserveCreationInfo { * Number of currently offered denominations. */ numOfferedDenoms: number; + /** * Public keys of trusted auditors for the currency we're withdrawing. */ @@ -337,10 +325,11 @@ export class CreateReserveRequest { exchange: string; /** - * Wire details for the bank account that sent the funds to the exchange. + * Wire details (as a payto URI) for the bank account that sent the funds to + * the exchange. */ - @Checkable.Optional(Checkable.Any()) - senderWire?: object; + @Checkable.Optional(Checkable.String()) + senderWire?: string; /** * Verify that a value matches the schema of this class and convert it into a diff --git a/src/webex/pages/confirm-create-reserve.tsx b/src/webex/pages/confirm-create-reserve.tsx index cef647163..2d4f41dfe 100644 --- a/src/webex/pages/confirm-create-reserve.tsx +++ b/src/webex/pages/confirm-create-reserve.tsx @@ -92,7 +92,7 @@ interface ExchangeSelectionProps { callback_url: string; wt_types: string[]; currencyRecord: CurrencyRecord|null; - sender_wire: object | undefined; + sender_wire: string | undefined; } interface ManualSelectionProps { @@ -410,7 +410,7 @@ class ExchangeSelection extends ImplicitStateComponent { exchange: string, amount: AmountJson, callback_url: string, - sender_wire: object | undefined) { + sender_wire: string | undefined) { const rawResp = await createReserve({ amount, exchange: canonicalizeBaseUrl(exchange), @@ -420,21 +420,25 @@ class ExchangeSelection extends ImplicitStateComponent { throw Error("empty response"); } // FIXME: filter out types that bank/exchange don't have in common - const wireDetails = rci.wireInfo; - const filteredWireDetails: any = {}; - for (const wireType in wireDetails) { - if (this.props.wt_types.findIndex((x) => x.toLowerCase() === wireType.toLowerCase()) < 0) { + const exchangeWireAccounts = []; + + for (let acct of rci.exchangeWireAccounts) { + const payto = new URI(acct); + if (payto.scheme() != "payto") { + console.warn("unknown wire account URI scheme", acct); continue; } - const obj = Object.assign({}, wireDetails[wireType]); - // The bank doesn't need to know about fees - delete obj.fees; - // Consequently the bank can't verify signatures anyway, so - // we delete this extra data, to make the request URL shorter. - delete obj.salt; - delete obj.sig; - filteredWireDetails[wireType] = obj; + if (this.props.wt_types.includes(payto.authority())) { + exchangeWireAccounts.push(acct); + } } + + const chosenAcct = exchangeWireAccounts[0]; + + if (!chosenAcct) { + throw Error("no exchange account matches the bank's supported types"); + } + if (!rawResp.error) { const resp = CreateReserveResponse.checked(rawResp); const q: {[name: string]: string|number} = { @@ -442,7 +446,7 @@ class ExchangeSelection extends ImplicitStateComponent { amount_fraction: amount.fraction, amount_value: amount.value, exchange: resp.exchange, - exchange_wire_details: JSON.stringify(filteredWireDetails), + exchange_wire_details: chosenAcct, reserve_pub: resp.reservePub, }; const url = new URI(callback_url).addQuery(q); @@ -487,7 +491,11 @@ async function main() { let sender_wire; if (query.sender_wire) { - sender_wire = JSON.parse(query.sender_wire); + let senderWireUri = new URI(query.sender_wire); + if (senderWireUri.scheme() != "payto") { + throw Error("sender wire info must be a payto URI"); + } + sender_wire = query.sender_wire; } const suggestedExchangeUrl = query.suggested_exchange_url; diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts index fde7b8c35..4f7500368 100644 --- a/src/webex/wxApi.ts +++ b/src/webex/wxApi.ts @@ -273,7 +273,7 @@ export function checkUpgrade(): Promise { /** * Create a reserve. */ -export function createReserve(args: { amount: AmountJson, exchange: string, senderWire?: object }): Promise { +export function createReserve(args: { amount: AmountJson, exchange: string, senderWire?: string }): Promise { return callBackend("create-reserve", args); } diff --git a/webpack.config.js b/webpack.config.js index 68e8b3cac..845b56fe2 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -18,7 +18,7 @@ module.exports = function (env) { const base = { output: { filename: '[name]-bundle.js', - chunkFilename: "[id]-bundle.js", + chunkFilename: "[name]-bundle.js", path: path.resolve(__dirname, "dist"), }, module: {