implement payto URIs

This commit is contained in:
Florian Dold 2019-05-08 04:53:26 +02:00
parent 3db38fbd0b
commit 6904c2759e
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
13 changed files with 82 additions and 118 deletions

View File

@ -34,7 +34,6 @@ import {
MerchantRefundPermission, MerchantRefundPermission,
PayReq, PayReq,
TipResponse, TipResponse,
WireDetail,
} from "./talerTypes"; } from "./talerTypes";
import { import {
@ -114,10 +113,10 @@ export interface ReserveRecord {
hasPayback: boolean; hasPayback: boolean;
/** /**
* Wire information for the bank account that * Wire information (as payto URI) for the bank account that
* transfered funds for this reserve. * 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. * Information about wire information for bank accounts we withdrew coins from.
*/ */
export interface SenderWireRecord { export interface SenderWireRecord {
/** paytoUri: string;
* Wire details.
*/
senderWire: WireDetail;
/**
* Identifier, hash code of canonicalized senderWire.
*/
id: number;
} }
@ -1001,7 +992,7 @@ export namespace Stores {
class SenderWiresStore extends Store<SenderWireRecord> { class SenderWiresStore extends Store<SenderWireRecord> {
constructor() { constructor() {
super("senderWires", { keyPath: "id" }); super("senderWires", { keyPath: "paytoUri" });
} }
} }

View File

@ -235,7 +235,7 @@ msgstr ""
#. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-# #. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-#
#. TODO:generic error reporting function or component. #. 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 #, c-format
msgid "Fatal error: \"%1$s\"." msgid "Fatal error: \"%1$s\"."
msgstr "" msgstr ""

View File

@ -235,7 +235,7 @@ msgstr ""
#. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-# #. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-#
#. TODO:generic error reporting function or component. #. 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 #, c-format
msgid "Fatal error: \"%1$s\"." msgid "Fatal error: \"%1$s\"."
msgstr "" msgstr ""

View File

@ -235,7 +235,7 @@ msgstr ""
#. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-# #. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-#
#. TODO:generic error reporting function or component. #. 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 #, c-format
msgid "Fatal error: \"%1$s\"." msgid "Fatal error: \"%1$s\"."
msgstr "" msgstr ""

View File

@ -235,7 +235,7 @@ msgstr ""
#. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-# #. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-#
#. TODO:generic error reporting function or component. #. 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 #, c-format
msgid "Fatal error: \"%1$s\"." msgid "Fatal error: \"%1$s\"."
msgstr "" msgstr ""

View File

@ -239,7 +239,7 @@ msgstr ""
#. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-# #. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-#
#. TODO:generic error reporting function or component. #. 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 #, c-format
msgid "Fatal error: \"%1$s\"." msgid "Fatal error: \"%1$s\"."
msgstr "" msgstr ""

View File

@ -235,7 +235,7 @@ msgstr ""
#. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-# #. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-#
#. TODO:generic error reporting function or component. #. 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 #, c-format
msgid "Fatal error: \"%1$s\"." msgid "Fatal error: \"%1$s\"."
msgstr "" msgstr ""

View File

@ -852,29 +852,25 @@ export class WireFeesJson {
} }
/** @Checkable.Class()
* Information about wire transfer methods supported export class AccountInfo {
* by the exchange.
*/
@Checkable.Class({extra: true})
export class WireDetailJson {
/**
* Name of the wire transfer method.
*/
@Checkable.String() @Checkable.String()
type: string; url: string;
/** @Checkable.String()
* Fees associated with the wire transfer method. master_sig: string;
*/ }
@Checkable.List(Checkable.Value(() => WireFeesJson))
fees: WireFeesJson[];
/**
* Verify that a value matches the schema of this class and convert it into a @Checkable.Class({extra: true})
* member. export class ExchangeWireJson {
*/ @Checkable.Map(Checkable.String(), Checkable.List(Checkable.Value(() => WireFeesJson)))
static checked: (obj: any) => WireDetailJson; 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 }; 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. * Proposal returned from the contract URL.

View File

@ -28,7 +28,6 @@ import {
canonicalJson, canonicalJson,
canonicalizeBaseUrl, canonicalizeBaseUrl,
getTalerStampSec, getTalerStampSec,
hash,
strcmp, strcmp,
} from "./helpers"; } from "./helpers";
import { import {
@ -75,6 +74,7 @@ import {
ContractTerms, ContractTerms,
Denomination, Denomination,
ExchangeHandle, ExchangeHandle,
ExchangeWireJson,
KeysJson, KeysJson,
MerchantRefundPermission, MerchantRefundPermission,
MerchantRefundResponse, MerchantRefundResponse,
@ -86,8 +86,6 @@ import {
TipPlanchetDetail, TipPlanchetDetail,
TipResponse, TipResponse,
TipToken, TipToken,
WireDetailJson,
isWireDetail,
} from "./talerTypes"; } from "./talerTypes";
import { import {
Badge, Badge,
@ -109,7 +107,6 @@ import {
TipStatus, TipStatus,
WalletBalance, WalletBalance,
WalletBalanceEntry, WalletBalanceEntry,
WireInfo,
} from "./walletTypes"; } from "./walletTypes";
@ -1133,10 +1130,9 @@ export class Wallet {
}; };
const senderWire = req.senderWire; const senderWire = req.senderWire;
if (isWireDetail(senderWire)) { if (senderWire) {
const rec = { const rec = {
id: hash(senderWire), paytoUri: senderWire,
senderWire,
}; };
await this.q().put(Stores.senderWires, rec).finish(); 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. * Get the wire information for the exchange with the given base URL.
*/ */
async getWireInfo(exchangeBaseUrl: string): Promise<WireInfo> { async getWireInfo(exchangeBaseUrl: string): Promise<ExchangeWireJson> {
exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl); exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
const reqUrl = new URI("wire").absoluteTo(exchangeBaseUrl); const reqUrl = new URI("wire").absoluteTo(exchangeBaseUrl);
const resp = await this.http.get(reqUrl.href()); const resp = await this.http.get(reqUrl.href());
@ -1340,7 +1336,7 @@ export class Wallet {
if (!wiJson) { if (!wiJson) {
throw Error("/wire response malformed"); 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}`); 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); const {isTrusted, isAudited} = await this.getExchangeTrust(exchangeInfo);
let earliestDepositExpiration = Infinity; let earliestDepositExpiration = Infinity;
@ -1534,10 +1535,10 @@ export class Wallet {
} }
} }
const ret: ReserveCreationInfo = { const ret: ReserveCreationInfo = {
earliestDepositExpiration, earliestDepositExpiration,
exchangeInfo, exchangeInfo,
exchangeWireAccounts,
exchangeVersion: exchangeInfo.protocolVersion || "unknown", exchangeVersion: exchangeInfo.protocolVersion || "unknown",
isAudited, isAudited,
isTrusted, isTrusted,
@ -1548,7 +1549,6 @@ export class Wallet {
versionMatch, versionMatch,
walletVersion: WALLET_PROTOCOL_VERSION, walletVersion: WALLET_PROTOCOL_VERSION,
wireFees, wireFees,
wireInfo,
withdrawFee: acc, withdrawFee: acc,
}; };
return ret; return ret;
@ -1563,26 +1563,13 @@ export class Wallet {
async updateExchangeFromUrl(baseUrl: string): Promise<ExchangeRecord> { async updateExchangeFromUrl(baseUrl: string): Promise<ExchangeRecord> {
baseUrl = canonicalizeBaseUrl(baseUrl); baseUrl = canonicalizeBaseUrl(baseUrl);
const keysUrl = new URI("keys").absoluteTo(baseUrl); const keysUrl = new URI("keys").absoluteTo(baseUrl);
const wireUrl = new URI("wire").absoluteTo(baseUrl);
const keysResp = await this.http.get(keysUrl.href()); const keysResp = await this.http.get(keysUrl.href());
if (keysResp.status !== 200) { if (keysResp.status !== 200) {
throw Error("/keys request failed"); 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 exchangeKeysJson = KeysJson.checked(JSON.parse(keysResp.responseText));
const wireRespJson = JSON.parse(wireResp.responseText); const exchangeWire = await this.getWireInfo(baseUrl);
if (typeof wireRespJson !== "object") { return this.updateExchangeFromJson(baseUrl, exchangeKeysJson, exchangeWire);
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);
} }
@ -1614,7 +1601,7 @@ export class Wallet {
private async updateExchangeFromJson(baseUrl: string, private async updateExchangeFromJson(baseUrl: string,
exchangeKeysJson: KeysJson, exchangeKeysJson: KeysJson,
wireMethodDetails: WireDetailJson[]): Promise<ExchangeRecord> { wireMethodDetails: ExchangeWireJson): Promise<ExchangeRecord> {
// FIXME: all this should probably be commited atomically // FIXME: all this should probably be commited atomically
const updateTimeSec = getTalerStampSec(exchangeKeysJson.list_issue_date); 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; let latestFeeStamp = 0;
const fees = oldWireFees.feesForType[detail.type] || []; const newFeeDetails = wireMethodDetails.fees[paytoTargetType];
oldWireFees.feesForType[detail.type] = fees; const oldFeeDetails = oldWireFees.feesForType[paytoTargetType] || [];
for (const oldFee of fees) { oldWireFees.feesForType[paytoTargetType] = oldFeeDetails;
for (const oldFee of oldFeeDetails) {
if (oldFee.endStamp > latestFeeStamp) { if (oldFee.endStamp > latestFeeStamp) {
latestFeeStamp = oldFee.endStamp; latestFeeStamp = oldFee.endStamp;
} }
} }
for (const fee of detail.fees) { for (const fee of newFeeDetails) {
const start = getTalerStampSec(fee.start_date); const start = getTalerStampSec(fee.start_date);
if (start === null) { if (start === null) {
console.error("invalid start stamp in fee", fee); console.error("invalid start stamp in fee", fee);
@ -1697,12 +1685,12 @@ export class Wallet {
startStamp: start, startStamp: start,
wireFee: Amounts.parseOrThrow(fee.wire_fee), 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) { if (!valid) {
console.error("fee signature invalid", fee); console.error("fee signature invalid", fee);
throw Error("fee signature invalid"); throw Error("fee signature invalid");
} }
fees.push(wf); oldFeeDetails.push(wf);
} }
} }
@ -2434,11 +2422,9 @@ export class Wallet {
const senderWiresSet = new Set(); const senderWiresSet = new Set();
await this.q().iter(Stores.senderWires).map((x) => { await this.q().iter(Stores.senderWires).map((x) => {
if (x.senderWire) { senderWiresSet.add(x.paytoUri);
senderWiresSet.add(canonicalJson(x.senderWire));
}
}).run(); }).run();
const senderWires = Array.from(senderWiresSet).map((x) => JSON.parse(x)); const senderWires = Array.from(senderWiresSet);
return { return {
exchangeWireTypes, exchangeWireTypes,

View File

@ -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. * Information about what will happen when creating a reserve.
* *
@ -97,7 +84,7 @@ export interface ReserveCreationInfo {
/** /**
* Filtered wire info to send to the bank. * Filtered wire info to send to the bank.
*/ */
wireInfo: WireInfo; exchangeWireAccounts: string[];
/** /**
* Selected denominations for withdraw. * Selected denominations for withdraw.
@ -139,6 +126,7 @@ export interface ReserveCreationInfo {
* Number of currently offered denominations. * Number of currently offered denominations.
*/ */
numOfferedDenoms: number; numOfferedDenoms: number;
/** /**
* Public keys of trusted auditors for the currency we're withdrawing. * Public keys of trusted auditors for the currency we're withdrawing.
*/ */
@ -337,10 +325,11 @@ export class CreateReserveRequest {
exchange: string; 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()) @Checkable.Optional(Checkable.String())
senderWire?: object; senderWire?: string;
/** /**
* Verify that a value matches the schema of this class and convert it into a * Verify that a value matches the schema of this class and convert it into a

View File

@ -92,7 +92,7 @@ interface ExchangeSelectionProps {
callback_url: string; callback_url: string;
wt_types: string[]; wt_types: string[];
currencyRecord: CurrencyRecord|null; currencyRecord: CurrencyRecord|null;
sender_wire: object | undefined; sender_wire: string | undefined;
} }
interface ManualSelectionProps { interface ManualSelectionProps {
@ -410,7 +410,7 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
exchange: string, exchange: string,
amount: AmountJson, amount: AmountJson,
callback_url: string, callback_url: string,
sender_wire: object | undefined) { sender_wire: string | undefined) {
const rawResp = await createReserve({ const rawResp = await createReserve({
amount, amount,
exchange: canonicalizeBaseUrl(exchange), exchange: canonicalizeBaseUrl(exchange),
@ -420,21 +420,25 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
throw Error("empty response"); throw Error("empty response");
} }
// FIXME: filter out types that bank/exchange don't have in common // FIXME: filter out types that bank/exchange don't have in common
const wireDetails = rci.wireInfo; const exchangeWireAccounts = [];
const filteredWireDetails: any = {};
for (const wireType in wireDetails) { for (let acct of rci.exchangeWireAccounts) {
if (this.props.wt_types.findIndex((x) => x.toLowerCase() === wireType.toLowerCase()) < 0) { const payto = new URI(acct);
if (payto.scheme() != "payto") {
console.warn("unknown wire account URI scheme", acct);
continue; continue;
} }
const obj = Object.assign({}, wireDetails[wireType]); if (this.props.wt_types.includes(payto.authority())) {
// The bank doesn't need to know about fees exchangeWireAccounts.push(acct);
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;
} }
}
const chosenAcct = exchangeWireAccounts[0];
if (!chosenAcct) {
throw Error("no exchange account matches the bank's supported types");
}
if (!rawResp.error) { if (!rawResp.error) {
const resp = CreateReserveResponse.checked(rawResp); const resp = CreateReserveResponse.checked(rawResp);
const q: {[name: string]: string|number} = { const q: {[name: string]: string|number} = {
@ -442,7 +446,7 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
amount_fraction: amount.fraction, amount_fraction: amount.fraction,
amount_value: amount.value, amount_value: amount.value,
exchange: resp.exchange, exchange: resp.exchange,
exchange_wire_details: JSON.stringify(filteredWireDetails), exchange_wire_details: chosenAcct,
reserve_pub: resp.reservePub, reserve_pub: resp.reservePub,
}; };
const url = new URI(callback_url).addQuery(q); const url = new URI(callback_url).addQuery(q);
@ -487,7 +491,11 @@ async function main() {
let sender_wire; let sender_wire;
if (query.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; const suggestedExchangeUrl = query.suggested_exchange_url;

View File

@ -273,7 +273,7 @@ export function checkUpgrade(): Promise<UpgradeResponse> {
/** /**
* Create a reserve. * Create a reserve.
*/ */
export function createReserve(args: { amount: AmountJson, exchange: string, senderWire?: object }): Promise<any> { export function createReserve(args: { amount: AmountJson, exchange: string, senderWire?: string }): Promise<any> {
return callBackend("create-reserve", args); return callBackend("create-reserve", args);
} }

View File

@ -18,7 +18,7 @@ module.exports = function (env) {
const base = { const base = {
output: { output: {
filename: '[name]-bundle.js', filename: '[name]-bundle.js',
chunkFilename: "[id]-bundle.js", chunkFilename: "[name]-bundle.js",
path: path.resolve(__dirname, "dist"), path: path.resolve(__dirname, "dist"),
}, },
module: { module: {