Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
40.68% covered (danger)
40.68%
24 / 59
16.67% covered (danger)
16.67%
2 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
Bitcoin_Address
40.68% covered (danger)
40.68%
24 / 59
16.67% covered (danger)
16.67%
2 / 12
179.19
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 get_wallet_parent_post_id
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_derivation_path_sequence_number
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 get_raw_address
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_blockchain_transactions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 set_transactions
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 get_balance
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 get_amount_received
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_status
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_status
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
3.04
 get_order_id
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 set_order_id
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
1<?php
2/**
3 *
4 *
5 * TODO: Update the wp_post last modified time when updating metadata.
6 *
7 * @package    brianhenryie/bh-wp-bitcoin-gateway
8 */
9
10namespace BrianHenryIE\WP_Bitcoin_Gateway\API\Addresses;
11
12use BrianHenryIE\WP_Bitcoin_Gateway\API\Model\Transaction_Interface;
13use DateTimeInterface;
14use Exception;
15use BrianHenryIE\WP_Bitcoin_Gateway\Admin\Addresses_List_Table;
16use BrianHenryIE\WP_Bitcoin_Gateway\WooCommerce\Bitcoin_Gateway;
17use RuntimeException;
18use InvalidArgumentException;
19use WP_Post;
20
21/**
22 * Facade on WP_Post and post_meta.
23 */
24class Bitcoin_Address {
25
26    const POST_TYPE = 'bh-bitcoin-address';
27
28    const TRANSACTION_META_KEY                     = 'address_transactions';
29    const DERIVATION_PATH_SEQUENCE_NUMBER_META_KEY = 'derivation_path_sequence_number';
30    const BALANCE_META_KEY                         = 'balance';
31    const ORDER_ID_META_KEY                        = 'order_id';
32
33    /**
34     * The wp_post database row, as a WordPress post object, for the custom post type used to store the data.
35     *
36     * @var WP_Post
37     */
38    protected WP_Post $post;
39
40    protected int $post_id;
41    protected string $status;
42    protected int $wallet_parent_post_id;
43    protected ?int $derivation_path_sequence_number;
44    protected string $raw_address;
45
46    /** @var array<string,Transaction_Interface> */
47    protected ?array $transactions;
48    /**
49     * @var mixed|string|null
50     */
51    protected $balance;
52    protected ?int $order_id;
53
54
55    /**
56     * Constructor
57     *
58     * @param int $post_id The wp_post ID the Bitcoin address detail is stored under.
59     *
60     * @throws Exception When the supplied post_id is not a post of this type.
61     */
62    public function __construct( int $post_id ) {
63        $post = get_post( $post_id );
64        if ( ! ( $post instanceof WP_Post ) || self::POST_TYPE !== $post->post_type ) {
65            throw new InvalidArgumentException( 'post_id ' . $post_id . ' is not a ' . self::POST_TYPE . ' post object' );
66        }
67
68        $this->post                            = $post;
69        $this->post_id                         = $post_id;
70        $this->wallet_parent_post_id           = $this->post->post_parent;
71        $this->status                          = $this->post->post_status;
72        $this->derivation_path_sequence_number = (int) get_post_meta( $post_id, self::DERIVATION_PATH_SEQUENCE_NUMBER_META_KEY, true );
73        $this->raw_address                     = $this->post->post_excerpt;
74        $this->transactions                    = get_post_meta( $post_id, self::TRANSACTION_META_KEY, true ) ?: null;
75        $this->balance                         = get_post_meta( $post_id, self::BALANCE_META_KEY, true );
76        $this->order_id                        = intval( get_post_meta( $post_id, self::ORDER_ID_META_KEY, true ) );
77    }
78
79    /**
80     * The post ID for the xpub|ypub|zpub wallet this address was derived for.
81     *
82     * @return int
83     */
84    public function get_wallet_parent_post_id(): int {
85        return $this->wallet_parent_post_id;
86    }
87
88    /**
89     * Get this Bitcoin address's derivation path.
90     *
91     * @readonly
92     */
93    public function get_derivation_path_sequence_number(): ?int {
94        return is_numeric( $this->derivation_path_sequence_number ) ? intval( $this->derivation_path_sequence_number ) : null;
95    }
96
97    /**
98     * Return the raw Bitcoin address this object represents.
99     *
100     * @used-by API::check_new_addresses_for_transactions() When verifying newly generated addresses have no existing transactions.
101     * @used-by API::get_fresh_address_for_order() When adding the payment address to the order meta.
102     * @used-by Bitcoin_Gateway::process_payment() When adding a link in the order notes to view transactions on a 3rd party website.
103     * @used-by API::update_address_transactions() When checking has an order been paid.
104     */
105    public function get_raw_address(): string {
106        return $this->raw_address;
107    }
108
109    /**
110     * Return the previously saved transactions for this address.
111     *
112     * @used-by API::update_address_transactions() When checking previously fetched transactions before a new query.
113     * @used-by API::get_order_details() When displaying the order/address details in the admin/frontend UI.
114     * @used-by Addresses_List_Table::print_columns() When displaying all addresses.
115     *
116     * @return array<string,Transaction_Interface>|null
117     */
118    public function get_blockchain_transactions(): ?array {
119        return is_array( $this->transactions ) ? $this->transactions : null;
120    }
121
122    // get_mempool_transactions()
123
124    /**
125     * Save the transactions recently fetched from the API.
126     *
127     * @used-by API::update_address_transactions()
128     *
129     * @param array<string,Transaction_Interface> $refreshed_transactions Array of the transaction details keyed by each transaction id.
130     */
131    public function set_transactions( array $refreshed_transactions ): void {
132
133        $update = array(
134            'ID'         => $this->post->ID,
135            'meta_input' => array(
136                self::TRANSACTION_META_KEY => $refreshed_transactions,
137            ),
138        );
139
140        if ( empty( $refreshed_transactions ) ) {
141            $update['post_status'] = 'unused';
142        } elseif ( 'unknown' === $this->get_status() ) {
143            $update['post_status'] = 'used';
144        }
145
146        $result = wp_update_post( $update );
147        if ( ! is_wp_error( $result ) ) {
148            $this->transactions = $update;
149        } else {
150            throw new RuntimeException( $result->get_error_message() );
151        }
152    }
153
154    /**
155     * Return the balance saved in the post meta, or null if the address status is unknown.
156     *
157     * TODO: Might need a $confirmations parameter and calculate the balance from the transactions.
158     *
159     * @used-by Addresses_List_Table::print_columns()
160     *
161     * @return ?string Null if unknown.
162     */
163    public function get_balance(): ?string {
164        $balance = empty( $this->balance ) ? '0.0' : $this->balance;
165        return 'unknown' === $this->get_status() ? null : $balance;
166    }
167
168    /**
169     * TODO: "balance" is not an accurate term for what we need.
170     */
171    public function get_amount_received(): ?string {
172        return $this->get_balance();
173    }
174
175    /**
176     * Return the current status of the Bitcoin address object. One of:
177     * * unknown: probably brand new and unchecked
178     * * unused: new and no order id assigned
179     * * assigned: assigned to an order, payment incomplete
180     * * used: transactions present and no order id, or and order id assigned and payment complete
181     *
182     * TODO: Check the saved status is valid.
183     *
184     * @return string unknown|unused|assigned|used.
185     */
186    public function get_status(): string {
187        return $this->status;
188    }
189
190    /**
191     * Set the current status of the address.
192     *
193     * Valid statuses: unknown|unused|assigned|used.
194     *
195     * TODO: Throw an exception if an invalid status is set. Maybe in the `wp_insert_post_data` filter.
196     * TODO: Maybe throw an exception if the update fails.
197     *
198     * @param string $status Status to assign.
199     */
200    public function set_status( string $status ): void {
201
202        if ( ! in_array( $status, array( 'unknown', 'unused', 'assigned', 'used' ), true ) ) {
203            throw new InvalidArgumentException( "{$status} should be one of unknown|unused|assigned|used" );
204        }
205
206        $result = wp_update_post(
207            array(
208                'post_type'   => self::POST_TYPE,
209                'ID'          => $this->post->ID,
210                'post_status' => $status,
211            )
212        );
213
214        if ( ! is_wp_error( $result ) ) {
215            $this->status = $status;
216        } else {
217            throw new RuntimeException( $result );
218        }
219    }
220
221    /**
222     * Get the order id associated with this address, or null if none has ever been assigned.
223     *
224     * @return ?int
225     */
226    public function get_order_id(): ?int {
227        return 0 === $this->order_id ? null : $this->order_id;
228    }
229
230    /**
231     * Add order_id metadata to the bitcoin address and update the status to assigned.
232     *
233     * @param int $order_id The WooCommerce order id the address is being used for.
234     */
235    public function set_order_id( int $order_id ): void {
236
237        $update = array(
238            'ID'         => $this->post->ID,
239            'meta_input' => array(
240                self::ORDER_ID_META_KEY => $order_id,
241            ),
242        );
243
244        if ( 'assigned' !== $this->get_status() ) {
245            $update['post_status'] = 'assigned';
246        }
247
248        $result = wp_update_post( $update );
249        if ( ! is_wp_error( $result ) ) {
250            $this->order_id = $order_id;
251        } else {
252            throw new RuntimeException( $result->get_error_message() );
253        }
254    }
255}