diff --git a/wallet/db.c b/wallet/db.c index ecc4db49a..a8f5305ba 100644 --- a/wallet/db.c +++ b/wallet/db.c @@ -657,6 +657,13 @@ static struct migration dbmigrations[] = { " cause INTEGER," " message TEXT" ");"), NULL}, + {SQL("CREATE TABLE offers (" + " offer_id BLOB" + ", bolt12 TEXT" + ", label TEXT" + ", status INTEGER" + ", PRIMARY KEY (offer_id)" + ");"), NULL}, }; /* Leak tracking. */ diff --git a/wallet/db_postgres_sqlgen.c b/wallet/db_postgres_sqlgen.c index 767d36123..28e88d554 100644 --- a/wallet/db_postgres_sqlgen.c +++ b/wallet/db_postgres_sqlgen.c @@ -848,6 +848,12 @@ struct db_query db_postgres_queries[] = { .placeholders = 0, .readonly = false, }, + { + .name = "CREATE TABLE offers ( offer_id BLOB, bolt12 TEXT, label TEXT, status INTEGER, PRIMARY KEY (offer_id));", + .query = "CREATE TABLE offers ( offer_id BYTEA, bolt12 TEXT, label TEXT, status INTEGER, PRIMARY KEY (offer_id));", + .placeholders = 0, + .readonly = false, + }, { .name = "UPDATE vars SET intval = intval + 1 WHERE name = 'data_version' AND intval = ?", .query = "UPDATE vars SET intval = intval + 1 WHERE name = 'data_version' AND intval = $1", @@ -1664,6 +1670,48 @@ struct db_query db_postgres_queries[] = { .placeholders = 2, .readonly = false, }, + { + .name = "SELECT 1 FROM offers WHERE offer_id = ?;", + .query = "SELECT 1 FROM offers WHERE offer_id = $1;", + .placeholders = 1, + .readonly = true, + }, + { + .name = "INSERT INTO offers ( offer_id, bolt12, label, status) VALUES (?, ?, ?, ?);", + .query = "INSERT INTO offers ( offer_id, bolt12, label, status) VALUES ($1, $2, $3, $4);", + .placeholders = 4, + .readonly = false, + }, + { + .name = "SELECT bolt12, label, status FROM offers WHERE offer_id = ?;", + .query = "SELECT bolt12, label, status FROM offers WHERE offer_id = $1;", + .placeholders = 1, + .readonly = true, + }, + { + .name = "SELECT offer_id FROM offers;", + .query = "SELECT offer_id FROM offers;", + .placeholders = 0, + .readonly = true, + }, + { + .name = "UPDATE offers SET status=? WHERE offer_id = ?;", + .query = "UPDATE offers SET status=$1 WHERE offer_id = $2;", + .placeholders = 2, + .readonly = false, + }, + { + .name = "UPDATE invoices SET state=? WHERE state=? AND offer_id = ?;", + .query = "UPDATE invoices SET state=$1 WHERE state=$2 AND offer_id = $3;", + .placeholders = 3, + .readonly = false, + }, + { + .name = "SELECT status FROM offers WHERE offer_id = ?;", + .query = "SELECT status FROM offers WHERE offer_id = $1;", + .placeholders = 1, + .readonly = true, + }, { .name = "SELECT name FROM sqlite_master WHERE type='table';", .query = "SELECT name FROM sqlite_master WHERE type='table';", @@ -1684,10 +1732,10 @@ struct db_query db_postgres_queries[] = { }, }; -#define DB_POSTGRES_QUERY_COUNT 279 +#define DB_POSTGRES_QUERY_COUNT 287 #endif /* HAVE_POSTGRES */ #endif /* LIGHTNINGD_WALLET_GEN_DB_POSTGRES */ -// SHA256STAMP:a51ae4928fd56f2b22d0dd8f498f1288e2c3df7be872ddf1240f4b8fded6fd22 +// SHA256STAMP:94af743741cd2a2d8725371af12e130a692334a118b52386fb901fadfe25d4cd diff --git a/wallet/db_sqlite3_sqlgen.c b/wallet/db_sqlite3_sqlgen.c index cececbef2..b1331d3b2 100644 --- a/wallet/db_sqlite3_sqlgen.c +++ b/wallet/db_sqlite3_sqlgen.c @@ -848,6 +848,12 @@ struct db_query db_sqlite3_queries[] = { .placeholders = 0, .readonly = false, }, + { + .name = "CREATE TABLE offers ( offer_id BLOB, bolt12 TEXT, label TEXT, status INTEGER, PRIMARY KEY (offer_id));", + .query = "CREATE TABLE offers ( offer_id BLOB, bolt12 TEXT, label TEXT, status INTEGER, PRIMARY KEY (offer_id));", + .placeholders = 0, + .readonly = false, + }, { .name = "UPDATE vars SET intval = intval + 1 WHERE name = 'data_version' AND intval = ?", .query = "UPDATE vars SET intval = intval + 1 WHERE name = 'data_version' AND intval = ?", @@ -1664,6 +1670,48 @@ struct db_query db_sqlite3_queries[] = { .placeholders = 2, .readonly = false, }, + { + .name = "SELECT 1 FROM offers WHERE offer_id = ?;", + .query = "SELECT 1 FROM offers WHERE offer_id = ?;", + .placeholders = 1, + .readonly = true, + }, + { + .name = "INSERT INTO offers ( offer_id, bolt12, label, status) VALUES (?, ?, ?, ?);", + .query = "INSERT INTO offers ( offer_id, bolt12, label, status) VALUES (?, ?, ?, ?);", + .placeholders = 4, + .readonly = false, + }, + { + .name = "SELECT bolt12, label, status FROM offers WHERE offer_id = ?;", + .query = "SELECT bolt12, label, status FROM offers WHERE offer_id = ?;", + .placeholders = 1, + .readonly = true, + }, + { + .name = "SELECT offer_id FROM offers;", + .query = "SELECT offer_id FROM offers;", + .placeholders = 0, + .readonly = true, + }, + { + .name = "UPDATE offers SET status=? WHERE offer_id = ?;", + .query = "UPDATE offers SET status=? WHERE offer_id = ?;", + .placeholders = 2, + .readonly = false, + }, + { + .name = "UPDATE invoices SET state=? WHERE state=? AND offer_id = ?;", + .query = "UPDATE invoices SET state=? WHERE state=? AND offer_id = ?;", + .placeholders = 3, + .readonly = false, + }, + { + .name = "SELECT status FROM offers WHERE offer_id = ?;", + .query = "SELECT status FROM offers WHERE offer_id = ?;", + .placeholders = 1, + .readonly = true, + }, { .name = "SELECT name FROM sqlite_master WHERE type='table';", .query = "SELECT name FROM sqlite_master WHERE type='table';", @@ -1684,10 +1732,10 @@ struct db_query db_sqlite3_queries[] = { }, }; -#define DB_SQLITE3_QUERY_COUNT 279 +#define DB_SQLITE3_QUERY_COUNT 287 #endif /* HAVE_SQLITE3 */ #endif /* LIGHTNINGD_WALLET_GEN_DB_SQLITE3 */ -// SHA256STAMP:a51ae4928fd56f2b22d0dd8f498f1288e2c3df7be872ddf1240f4b8fded6fd22 +// SHA256STAMP:94af743741cd2a2d8725371af12e130a692334a118b52386fb901fadfe25d4cd diff --git a/wallet/statements_gettextgen.po b/wallet/statements_gettextgen.po index 7d95510b8..9b76e0113 100644 --- a/wallet/statements_gettextgen.po +++ b/wallet/statements_gettextgen.po @@ -558,67 +558,71 @@ msgstr "" msgid "CREATE TABLE channel_state_changes ( channel_id BIGINT REFERENCES channels(id) ON DELETE CASCADE, timestamp BIGINT, old_state INTEGER, new_state INTEGER, cause INTEGER, message TEXT);" msgstr "" -#: wallet/db.c:886 +#: wallet/db.c:660 +msgid "CREATE TABLE offers ( offer_id BLOB, bolt12 TEXT, label TEXT, status INTEGER, PRIMARY KEY (offer_id));" +msgstr "" + +#: wallet/db.c:893 msgid "UPDATE vars SET intval = intval + 1 WHERE name = 'data_version' AND intval = ?" msgstr "" -#: wallet/db.c:986 +#: wallet/db.c:993 msgid "SELECT version FROM version LIMIT 1" msgstr "" -#: wallet/db.c:1044 +#: wallet/db.c:1051 msgid "UPDATE version SET version=?;" msgstr "" -#: wallet/db.c:1052 +#: wallet/db.c:1059 msgid "INSERT INTO db_upgrades VALUES (?, ?);" msgstr "" -#: wallet/db.c:1064 +#: wallet/db.c:1071 msgid "SELECT intval FROM vars WHERE name = 'data_version'" msgstr "" -#: wallet/db.c:1091 +#: wallet/db.c:1098 msgid "SELECT intval FROM vars WHERE name= ? LIMIT 1" msgstr "" -#: wallet/db.c:1107 +#: wallet/db.c:1114 msgid "UPDATE vars SET intval=? WHERE name=?;" msgstr "" -#: wallet/db.c:1116 +#: wallet/db.c:1123 msgid "INSERT INTO vars (name, intval) VALUES (?, ?);" msgstr "" -#: wallet/db.c:1130 +#: wallet/db.c:1137 msgid "UPDATE channels SET feerate_base = ?, feerate_ppm = ?;" msgstr "" -#: wallet/db.c:1151 +#: wallet/db.c:1158 msgid "UPDATE channels SET our_funding_satoshi = funding_satoshi WHERE funder = 0;" msgstr "" -#: wallet/db.c:1167 +#: wallet/db.c:1174 msgid "SELECT type, keyindex, prev_out_tx, prev_out_index, channel_id, peer_id, commitment_point FROM outputs WHERE scriptpubkey IS NULL;" msgstr "" -#: wallet/db.c:1229 +#: wallet/db.c:1236 msgid "UPDATE outputs SET scriptpubkey = ? WHERE prev_out_tx = ? AND prev_out_index = ?" msgstr "" -#: wallet/db.c:1254 +#: wallet/db.c:1261 msgid "SELECT id, funding_tx_id, funding_tx_outnum FROM channels;" msgstr "" -#: wallet/db.c:1273 +#: wallet/db.c:1280 msgid "UPDATE channels SET full_channel_id = ? WHERE id = ?;" msgstr "" -#: wallet/db.c:1296 +#: wallet/db.c:1303 msgid "SELECT c.id, p.node_id, c.last_tx, c.funding_satoshi, c.fundingkey_remote, c.last_sig FROM channels c LEFT OUTER JOIN peers p ON p.id = c.peer_id;" msgstr "" -#: wallet/db.c:1363 +#: wallet/db.c:1370 msgid "UPDATE channels SET last_tx = ? WHERE id = ?;" msgstr "" @@ -1102,6 +1106,34 @@ msgstr "" msgid "DELETE FROM penalty_bases WHERE channel_id = ? AND commitnum = ?" msgstr "" +#: wallet/wallet.c:3976 +msgid "SELECT 1 FROM offers WHERE offer_id = ?;" +msgstr "" + +#: wallet/wallet.c:3989 +msgid "INSERT INTO offers ( offer_id, bolt12, label, status) VALUES (?, ?, ?, ?);" +msgstr "" + +#: wallet/wallet.c:4017 +msgid "SELECT bolt12, label, status FROM offers WHERE offer_id = ?;" +msgstr "" + +#: wallet/wallet.c:4045 +msgid "SELECT offer_id FROM offers;" +msgstr "" + +#: wallet/wallet.c:4071 +msgid "UPDATE offers SET status=? WHERE offer_id = ?;" +msgstr "" + +#: wallet/wallet.c:4082 +msgid "UPDATE invoices SET state=? WHERE state=? AND offer_id = ?;" +msgstr "" + +#: wallet/wallet.c:4110 +msgid "SELECT status FROM offers WHERE offer_id = ?;" +msgstr "" + #: wallet/test/run-db.c:120 msgid "SELECT name FROM sqlite_master WHERE type='table';" msgstr "" @@ -1113,4 +1145,4 @@ msgstr "" #: wallet/test/run-wallet.c:1377 msgid "INSERT INTO channels (id) VALUES (1);" msgstr "" -# SHA256STAMP:ebfc7675e32b832d172d040d0120cd339a1e1e924969373f8d7d6b75a7836c5b +# SHA256STAMP:3c2b67d1ede6a371d5dfbc97c6406cb3152c0a36ff60d714967552de6d02f7d7 diff --git a/wallet/wallet.c b/wallet/wallet.c index a5187871a..b02527d51 100644 --- a/wallet/wallet.c +++ b/wallet/wallet.c @@ -3961,3 +3961,169 @@ void wallet_penalty_base_delete(struct wallet *w, u64 chan_id, u64 commitnum) db_bind_u64(stmt, 1, commitnum); db_exec_prepared_v2(take(stmt)); } + +bool wallet_offer_create(struct wallet *w, + const struct sha256 *offer_id, + const char *bolt12, + const struct json_escape *label, + enum offer_status status) +{ + struct db_stmt *stmt; + + assert(offer_status_active(status)); + + /* Test if already exists. */ + stmt = db_prepare_v2(w->db, SQL("SELECT 1" + " FROM offers" + " WHERE offer_id = ?;")); + db_bind_sha256(stmt, 0, offer_id); + db_query_prepared(stmt); + + if (db_step(stmt)) { + tal_free(stmt); + return false; + } + tal_free(stmt); + + stmt = db_prepare_v2(w->db, + SQL("INSERT INTO offers (" + " offer_id" + ", bolt12" + ", label" + ", status" + ") VALUES (?, ?, ?, ?);")); + + db_bind_sha256(stmt, 0, offer_id); + db_bind_text(stmt, 1, bolt12); + if (label) + db_bind_json_escape(stmt, 2, label); + else + db_bind_null(stmt, 2); + db_bind_int(stmt, 3, offer_status_in_db(status)); + db_exec_prepared_v2(take(stmt)); + return true; +} + +char *wallet_offer_find(const tal_t *ctx, + struct wallet *w, + const struct sha256 *offer_id, + const struct json_escape **label, + enum offer_status *status) +{ + struct db_stmt *stmt; + char *bolt12; + + /* Test if already exists. */ + stmt = db_prepare_v2(w->db, SQL("SELECT bolt12, label, status" + " FROM offers" + " WHERE offer_id = ?;")); + db_bind_sha256(stmt, 0, offer_id); + db_query_prepared(stmt); + + if (!db_step(stmt)) { + tal_free(stmt); + return NULL; + } + + bolt12 = tal_strdup(ctx, cast_signed(const char *, db_column_text(stmt, 0))); + if (label) { + if (db_column_is_null(stmt, 1)) + *label = NULL; + else + *label = db_column_json_escape(ctx, stmt, 1); + } + if (status) + *status = offer_status_in_db(db_column_int(stmt, 2)); + tal_free(stmt); + return bolt12; +} + +struct db_stmt *wallet_offer_first(struct wallet *w, struct sha256 *offer_id) +{ + struct db_stmt *stmt; + + stmt = db_prepare_v2(w->db, SQL("SELECT offer_id FROM offers;")); + db_query_prepared(stmt); + + return wallet_offer_next(w, stmt, offer_id); +} + +struct db_stmt *wallet_offer_next(struct wallet *w, + struct db_stmt *stmt, + struct sha256 *offer_id) +{ + if (!db_step(stmt)) + return tal_free(stmt); + + db_column_sha256(stmt, 0, offer_id); + return stmt; +} + +/* If we make an offer inactive, this also expires all invoices + * which we issued for it. */ +static void offer_status_update(struct db *db, + const struct sha256 *offer_id, + enum offer_status oldstatus, + enum offer_status newstatus) +{ + struct db_stmt *stmt; + + stmt = db_prepare_v2(db, SQL("UPDATE offers" + " SET status=?" + " WHERE offer_id = ?;")); + db_bind_int(stmt, 0, offer_status_in_db(newstatus)); + db_bind_sha256(stmt, 1, offer_id); + db_exec_prepared_v2(take(stmt)); + + if (!offer_status_active(oldstatus) + || offer_status_active(newstatus)) + return; + + stmt = db_prepare_v2(db, SQL("UPDATE invoices" + " SET state=?" + " WHERE state=? AND offer_id = ?;")); + db_bind_int(stmt, 0, invoice_status_in_db(UNPAID)); + db_bind_int(stmt, 1, invoice_status_in_db(EXPIRED)); + db_bind_sha256(stmt, 2, offer_id); + db_exec_prepared_v2(take(stmt)); +} + +enum offer_status wallet_offer_disable(struct wallet *w, + const struct sha256 *offer_id, + enum offer_status s) +{ + enum offer_status newstatus; + + assert(offer_status_active(s)); + + newstatus = offer_status_in_db(s &= ~OFFER_STATUS_ACTIVE_F); + offer_status_update(w->db, offer_id, s, newstatus); + + return newstatus; +} + +void wallet_offer_mark_used(struct db *db, const struct sha256 *offer_id) +{ + struct db_stmt *stmt; + enum offer_status status; + + stmt = db_prepare_v2(db, SQL("SELECT status" + " FROM offers" + " WHERE offer_id = ?;")); + db_bind_sha256(stmt, 0, offer_id); + db_query_prepared(stmt); + if (!db_step(stmt)) + fatal("Unknown stats offer_id %s", + type_to_string(tmpctx, struct sha256, offer_id)); + + status = offer_status_in_db(db_column_int(stmt, 0)); + tal_free(stmt); + + if (!offer_status_active(status)) + fatal("offer_id %s status %i", + type_to_string(tmpctx, struct sha256, offer_id), + status); + + if (status == OFFER_SINGLE_USE) + offer_status_update(db, offer_id, status, OFFER_USED); +} diff --git a/wallet/wallet.h b/wallet/wallet.h index 1a6782eaf..0dc5a53f5 100644 --- a/wallet/wallet.h +++ b/wallet/wallet.h @@ -1331,4 +1331,127 @@ struct penalty_base *wallet_penalty_base_load_for_channel(const tal_t *ctx, */ void wallet_penalty_base_delete(struct wallet *w, u64 chan_id, u64 commitnum); +/* /!\ This is a DB ENUM, please do not change the numbering of any + * already defined elements (adding is ok) /!\ */ +#define OFFER_STATUS_ACTIVE_F 0x1 +#define OFFER_STATUS_SINGLE_F 0x2 +#define OFFER_STATUS_USED_F 0x4 +enum offer_status { + OFFER_MULTIPLE_USE = OFFER_STATUS_ACTIVE_F, + OFFER_SINGLE_USE = OFFER_STATUS_ACTIVE_F|OFFER_STATUS_SINGLE_F, + OFFER_USED = OFFER_STATUS_SINGLE_F|OFFER_STATUS_USED_F, + OFFER_SINGLE_DISABLED = OFFER_STATUS_SINGLE_F, + OFFER_MULTIPLE_DISABLED = 0, +}; + +static inline enum offer_status offer_status_in_db(enum offer_status s) +{ + switch (s) { + case OFFER_MULTIPLE_USE: + BUILD_ASSERT(OFFER_MULTIPLE_USE == 1); + return s; + case OFFER_SINGLE_USE: + BUILD_ASSERT(OFFER_SINGLE_USE == 3); + return s; + case OFFER_USED: + BUILD_ASSERT(OFFER_USED == 6); + return s; + case OFFER_SINGLE_DISABLED: + BUILD_ASSERT(OFFER_SINGLE_DISABLED == 2); + return s; + case OFFER_MULTIPLE_DISABLED: + BUILD_ASSERT(OFFER_MULTIPLE_DISABLED == 0); + return s; + } + fatal("%s: %u is invalid", __func__, s); +} + +static inline bool offer_status_active(enum offer_status s) +{ + return s & OFFER_STATUS_ACTIVE_F; +} + +static inline bool offer_status_single(enum offer_status s) +{ + return s & OFFER_STATUS_SINGLE_F; +} + +/** + * Store an offer in the database. + * @w: the wallet + * @offer_id: the merkle root, as used for signing (must be unique) + * @bolt12: offer as text. + * @label: optional label for this offer. + * @status: OFFER_SINGLE_USE or OFFER_MULTIPLE_USE + */ +bool wallet_offer_create(struct wallet *w, + const struct sha256 *offer_id, + const char *bolt12, + const struct json_escape *label, + enum offer_status status) + NON_NULL_ARGS(1,2,3); + +/** + * Retrieve an offer from the database. + * @ctx: the tal context to allocate return from. + * @w: the wallet + * @offer_id: the merkle root, as used for signing (must be unique) + * @label: the label of the offer, set to NULL if none (or NULL) + * @status: set if succeeds (or NULL) + * + * If @offer_id is found, returns the bolt12 text, sets @label and + * @state. Otherwise returns NULL. + */ +char *wallet_offer_find(const tal_t *ctx, + struct wallet *w, + const struct sha256 *offer_id, + const struct json_escape **label, + enum offer_status *status) + NON_NULL_ARGS(1,2,3); + +/** + * Iterate through all the offers. + * @w: the wallet + * @offer_id: the first offer id (if returns non-NULL) + * + * Returns pointer to hand as @stmt to wallet_offer_next(), or NULL. + * If you choose not to call wallet_offer_next() you must free it! + */ +struct db_stmt *wallet_offer_first(struct wallet *w, + struct sha256 *offer_id); + +/** + * Iterate through all the offers. + * @w: the wallet + * @stmt: return from wallet_offer_first() or previous wallet_offer_next() + * @offer_id: the next offer id (if returns non-NULL) + * + * Returns NULL once we're out of offers. If you choose not to call + * wallet_offer_next() again you must free return. + */ +struct db_stmt *wallet_offer_next(struct wallet *w, + struct db_stmt *stmt, + struct sha256 *offer_id); + +/** + * Disable an offer in the database. + * @w: the wallet + * @offer_id: the merkle root, as used for signing (must be unique) + * @s: the current status (must be active). + * + * Must exist. Returns new status. */ +enum offer_status wallet_offer_disable(struct wallet *w, + const struct sha256 *offer_id, + enum offer_status s) + NO_NULL_ARGS; + +/** + * Mark an offer in the database used. + * @w: the wallet + * @offer_id: the merkle root, as used for signing (must be unique) + * + * Must exist and be active. + */ +void wallet_offer_mark_used(struct db *db, const struct sha256 *offer_id) + NO_NULL_ARGS; #endif /* LIGHTNING_WALLET_WALLET_H */