Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.43% covered (success)
91.43%
64 / 70
66.67% covered (warning)
66.67%
6 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Bitcoin_Wallet_Repository
91.43% covered (success)
91.43%
64 / 70
66.67% covered (warning)
66.67%
6 / 9
18.20
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_by_xpub
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 get_post_id_for_master_public_key
95.00% covered (success)
95.00%
19 / 20
0.00% covered (danger)
0.00%
0 / 1
5
 get_by_wp_post_id
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_all
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 save_new
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
5.01
 set_highest_address_index
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 refresh
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 append_gateway_details
60.00% covered (warning)
60.00%
6 / 10
0.00% covered (danger)
0.00%
0 / 1
1.06
1<?php
2/**
3 * Save new Bitcoin wallets in WordPress, and fetch them via xpub or post id.
4 *
5 * @package    brianhenryie/bh-wp-bitcoin-gateway
6 */
7
8namespace BrianHenryIE\WP_Bitcoin_Gateway\API\Repositories;
9
10use BrianHenryIE\WP_Bitcoin_Gateway\API\Model\Wallet\Bitcoin_Wallet;
11use BrianHenryIE\WP_Bitcoin_Gateway\API\Model\Wallet\Bitcoin_Wallet_WP_Post_Interface;
12use BrianHenryIE\WP_Bitcoin_Gateway\API\Repositories\Factories\Bitcoin_Wallet_Factory;
13use BrianHenryIE\WP_Bitcoin_Gateway\API\Repositories\Queries\Bitcoin_Wallet_Query;
14use BrianHenryIE\WP_Bitcoin_Gateway\API\Model\Wallet\Bitcoin_Wallet_Status;
15use BrianHenryIE\WP_Bitcoin_Gateway\API\Model\Exceptions\BH_WP_Bitcoin_Gateway_Exception;
16use BrianHenryIE\WP_Bitcoin_Gateway\Brick\Money\Exception\UnknownCurrencyException;
17use DateTimeImmutable;
18use InvalidArgumentException;
19use WP_Post;
20use wpdb;
21
22/**
23 * @see Bitcoin_Wallet_WP_Post_Interface
24 */
25class Bitcoin_Wallet_Repository extends WP_Post_Repository_Abstract {
26
27    /**
28     * Constructor.
29     *
30     * @param Bitcoin_Wallet_Factory $bitcoin_wallet_factory Factory for creating Bitcoin wallet objects.
31     */
32    public function __construct(
33        protected Bitcoin_Wallet_Factory $bitcoin_wallet_factory,
34    ) {
35    }
36
37    /**
38     * NB: post_name is 200 characters long. zpub is 111 characters.
39     *
40     * @param string $xpub The master public key of the wallet.
41     * @throws BH_WP_Bitcoin_Gateway_Exception If more than one saved wallet was found for the master public key.
42     * @throws UnknownCurrencyException If BTC is not correctly added to brick/money.
43     */
44    public function get_by_xpub( string $xpub ): ?Bitcoin_Wallet {
45
46        $post_id = $this->get_post_id_for_master_public_key( $xpub );
47
48        if ( ! $post_id ) {
49            return null;
50        }
51
52        return $this->bitcoin_wallet_factory->get_by_wp_post_id( $post_id );
53    }
54
55    /**
56     * Search wp_posts.post_name for the wallet master public key.
57     *
58     * @see wordpress/wp-admin/includes/schema.php:184
59     *
60     * @param string $master_public_key The Wallet address we may have saved.
61     *
62     * @throws BH_WP_Bitcoin_Gateway_Exception If there is more than one db entry for the same wallet (v.unlikely).
63     */
64    protected function get_post_id_for_master_public_key( string $master_public_key ): ?int {
65
66        $cached = wp_cache_get( $master_public_key, Bitcoin_Wallet_WP_Post_Interface::POST_TYPE );
67        if ( is_numeric( $cached ) ) {
68            return intval( $cached );
69        }
70
71        /** @var wpdb $wpdb */
72        global $wpdb;
73
74        /**
75         * phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
76         *
77         * @var array<int|numeric-string> $post_ids
78         */
79        $post_ids = $wpdb->get_col(
80            $wpdb->prepare(
81                'SELECT ID FROM %i WHERE post_name=%s AND post_type=%s',
82                $wpdb->posts,
83                sanitize_title( $master_public_key ),
84                Bitcoin_Wallet_WP_Post_Interface::POST_TYPE
85            )
86        );
87
88        switch ( count( $post_ids ) ) {
89            case 0:
90                return null;
91            case 1:
92                $post_id = intval( $post_ids[ array_key_first( $post_ids ) ] );
93                wp_cache_set( $master_public_key, $post_id, Bitcoin_Wallet_WP_Post_Interface::POST_TYPE );
94                return $post_id;
95            default:
96                throw new BH_WP_Bitcoin_Gateway_Exception( count( $post_ids ) . ' Bitcoin_Wallets found, only one expected, for ' . $master_public_key );
97        }
98    }
99
100    /**
101     * Given the id of the wp_posts row storing the bitcoin wallet, return the typed Bitcoin_Wallet object.
102     *
103     * @param int $post_id WordPress wp_posts ID.
104     *
105     * @return Bitcoin_Wallet
106     * @throws InvalidArgumentException When the post_type of the post returned for the given post_id is not a Bitcoin_Wallet.
107     */
108    public function get_by_wp_post_id( int $post_id ): Bitcoin_Wallet {
109        return $this->bitcoin_wallet_factory->get_by_wp_post_id( $post_id );
110    }
111
112    /**
113     *
114     * @param Bitcoin_Wallet_Status $status Filter by Bitcoin_Wallet_Status – 'active'|'inactive'.
115     *
116     * @return Bitcoin_Wallet[]
117     */
118    public function get_all( Bitcoin_Wallet_Status $status ): array {
119        $args = new Bitcoin_Wallet_Query(
120            status: $status,
121        );
122
123        $query_array = $args->to_query_array();
124        $query       = array_filter(
125            $query_array,
126            fn( string $key ): bool => in_array( $key, array( 'post_type', 'post_status' ), true ),
127            ARRAY_FILTER_USE_KEY,
128        );
129
130        /** @var WP_Post[] $posts */
131        $posts = get_posts( $query );
132
133        return array_map(
134            $this->bitcoin_wallet_factory->get_by_wp_post( ... ),
135            $posts
136        );
137    }
138
139    /**
140     * Create a new Bitcoin_Wallet WordPress post for the provided address and optionally specify the associated gateway.
141     *
142     * @param string                                              $master_public_key The xpub/ypub/zpub of the wallet.
143     * @param ?array{integration:class-string, gateway_id:string} $gateway The WC_Payment_Gateway the wallet is being used with.
144     *
145     * @return Bitcoin_Wallet The wp_posts saved wallet.
146     * @throws BH_WP_Bitcoin_Gateway_Exception When `wp_insert_post()` fails.
147     */
148    public function save_new( string $master_public_key, ?array $gateway = null ): Bitcoin_Wallet {
149
150        $existing = $this->get_by_xpub( $master_public_key );
151        if ( $existing ) {
152            return $existing;
153        }
154
155        $args = new Bitcoin_Wallet_Query(
156            master_public_key: $master_public_key,
157            status: ! is_null( $gateway ) ? Bitcoin_Wallet_Status::ACTIVE : Bitcoin_Wallet_Status::INACTIVE,
158            gateway_refs: $gateway ? array( $gateway ) : null,
159        );
160
161        $query_args_array = $args->to_query_array();
162        $post_id          = wp_insert_post( $query_args_array, true );
163
164        if ( is_wp_error( $post_id ) ) {
165            throw new BH_WP_Bitcoin_Gateway_Exception( 'Failed to save new wallet as wp_post' );
166        }
167
168        return $this->get_by_wp_post_id( $post_id );
169    }
170
171    /**
172     * Save the index of the highest generated address.
173     *
174     * @param Bitcoin_Wallet $wallet The Bitcoin Wallet to indicate its newest derived address index.
175     * @param int            $index Nth address generated index.
176     */
177    public function set_highest_address_index( Bitcoin_Wallet $wallet, int $index ): void {
178
179        $this->update(
180            model: $wallet,
181            query: new Bitcoin_Wallet_Query(
182                last_derived_address_index: $index,
183            )
184        );
185    }
186
187    /**
188     * Fetch the wallet from its wp_post again.
189     *
190     * @param Bitcoin_Wallet $wallet To refresh.
191     */
192    public function refresh( Bitcoin_Wallet $wallet ): Bitcoin_Wallet {
193        return $this->bitcoin_wallet_factory->get_by_wp_post_id( $wallet->get_post_id() );
194    }
195
196    /**
197     * Add meta to the wallet to record where it is being used.
198     *
199     * @param Bitcoin_Wallet                                     $wallet The wallet to update.
200     * @param array{integration:class-string, gateway_id:string} $new_gateway_details The integration,gateway_id to associate with the wallet.
201     * @return void
202     * @see Bitcoin_Wallet_WP_Post_Interface::GATEWAYS_DETAILS_META_KEY
203     */
204    public function append_gateway_details( Bitcoin_Wallet $wallet, array $new_gateway_details ): void {
205        $new_gateway_details['date_added'] = new DateTimeImmutable();
206
207        // TODO: does this need to be done for every one every time?
208        $new_gateway_details['integration'] = wp_slash( $new_gateway_details['integration'] );
209
210        $associated_gateway_details   = $wallet->get_associated_gateways_details();
211        $associated_gateway_details[] = $new_gateway_details;
212
213        $this->update(
214            $wallet,
215            new Bitcoin_Wallet_Query(
216                gateway_refs: $associated_gateway_details,
217            )
218        );
219    }
220}